mirror of
https://github.com/mit-regressions/viewer.git
synced 2025-04-20 11:00:16 -04:00
working prototype of metadata viewer with defined schema
This commit is contained in:
parent
4709d32be0
commit
46fd752f40
|
@ -2,8 +2,80 @@ WEBVTT
|
||||||
Kind: captions
|
Kind: captions
|
||||||
Language: en
|
Language: en
|
||||||
|
|
||||||
NOTE We could have a separation between MUSIC SOURCE, VIDEO SOURCE, NARRATION SOURCE, COMMENTARY, and TRANSCRIPT.
|
NOTE separation between MUSIC SOURCE, VIDEO SOURCE, NARRATION SOURCE, COMMENTARY, and TRANSCRIPT.
|
||||||
|
|
||||||
NOTE ["The Vanishing American Family" by ScubaZ]
|
|
||||||
00:00:01.550 --> 00:01:15.448
|
00:00:01.550 --> 00:01:15.448
|
||||||
{"keywords_alt": "", "gpspoints": {"gps_zoom": "11", "gps_text_alt": "", "gps_text": "Stockholm, Sweden", "gps": "59.3388502,18.0616981"}, "synopsis": "", "subjects": "", "hyperlinks": {"hyperlink_text_alt": "", "hyperlink_text": "", "hyperlink": ""}, "synopsis_alt": "", "title": "The Vanishing American Family", "keywords": "ScubaZ", "title_alt": "", "subjects_alt": ""}
|
{
|
||||||
|
"uid": "1",
|
||||||
|
"type": "music",
|
||||||
|
"data": {
|
||||||
|
"type": "music",
|
||||||
|
"title": "The Vanishing American Family",
|
||||||
|
"artist": "ScubaZ",
|
||||||
|
"year": "2001",
|
||||||
|
"label": "Odd Records Ltd.",
|
||||||
|
"hyperlink": "https://www.discogs.com/master/1184747-Scuba-Z-The-Vanishing-American-Family"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
00:00:17.550 --> 00:00:19.548
|
||||||
|
{
|
||||||
|
"uid": "2",
|
||||||
|
"type": "video_source",
|
||||||
|
"data": {
|
||||||
|
"type": "video_source",
|
||||||
|
"title": "\"We lived here, you didn't...\" @ Bexley Hall, MIT",
|
||||||
|
"artist": "Andrei Ivanov",
|
||||||
|
"year": "2013",
|
||||||
|
"notes": "AI-upscaled and frame-interpolated",
|
||||||
|
"retrieved_from": "YouTube",
|
||||||
|
"hyperlink": "https://www.youtube.com/watch?v=e6m8se96yyM&ab_channel=AndreiIvanov"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
00:00:47.000 --> 00:00:48.600
|
||||||
|
{
|
||||||
|
"uid": "2",
|
||||||
|
"type": "video_source",
|
||||||
|
"data": {
|
||||||
|
"type": "video_source",
|
||||||
|
"title": "The November Actions",
|
||||||
|
"artist": "Ricky Leacock",
|
||||||
|
"year": "1969",
|
||||||
|
"notes": "AI-upscaled and frame-interpolated",
|
||||||
|
"retrieved_from": "MIT Museum",
|
||||||
|
"hyperlink": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
00:00:56.000 --> 00:01:01.600
|
||||||
|
{
|
||||||
|
"uid": "3",
|
||||||
|
"type": "video_source",
|
||||||
|
"data": {
|
||||||
|
"type": "video_source",
|
||||||
|
"title": "MIT: Progressions",
|
||||||
|
"artist": "David and Sheri Espar",
|
||||||
|
"year": "1969",
|
||||||
|
"notes": "AI-upscaled and frame-interpolated",
|
||||||
|
"retrieved_from": "Kenneth Friedman (YouTube)",
|
||||||
|
"hyperlink": "https://www.youtube.com/watch?v=p3mq5E0GwLA&ab_channel=KennethFriedman"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
00:01:03.800 --> 00:01:15.600
|
||||||
|
{
|
||||||
|
"uid": "3",
|
||||||
|
"type": "video_source",
|
||||||
|
"data": {
|
||||||
|
"type": "video_source",
|
||||||
|
"title": "Technology",
|
||||||
|
"artist": "unknown creators",
|
||||||
|
"year": "1934",
|
||||||
|
"notes": "AI-upscaled and frame-interpolated. View Kathleen E. 23's <a href=\"https://mitadmissions.org/blogs/entry/5-historical-mit-videos/#video5\"><strong>post</strong></a>",
|
||||||
|
"retrieved_from": "MIT Museum",
|
||||||
|
"hyperlink": "https://www.youtube.com/watch?v=rsO_67xzymQ&ab_channel=FromtheVaultofMIT"
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,9 +14,6 @@ export default function Player() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div id="control-panel" className="flex flex-row">
|
|
||||||
<button id="show-metadata" className="bg-gray-200 hover:bg-gray-400 dark:bg-gray-800 dark:hover:bg-gray-700 text-gray-800 dark:text-gray-200 py-2 px-4">Show Metadata</button>
|
|
||||||
</div>
|
|
||||||
<WebVttPlayer
|
<WebVttPlayer
|
||||||
preload={false}
|
preload={false}
|
||||||
audio={audioUrl}
|
audio={audioUrl}
|
||||||
|
|
|
@ -2,85 +2,138 @@ import React, { Component } from 'react'
|
||||||
// import './MetadataPoint.css'
|
// import './MetadataPoint.css'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
type MetadataPointProps = {
|
type MetadataPointProps = {
|
||||||
cue: VTTCue
|
cue: VTTCue
|
||||||
active: boolean
|
active: boolean
|
||||||
seek: (time: number) => void
|
seek: (time: number) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
class MetadataPoint extends Component<MetadataPointProps> {
|
interface MetadataPointData {
|
||||||
|
"uid": string,
|
||||||
|
"type": string,
|
||||||
|
// data is one of four interfaces: music, commentary, transcript, or video_source
|
||||||
|
"data": MusicData | CommentaryData | TranscriptData | VideoSourceData | NarrationSourceData | OhmsData
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MusicData {
|
||||||
|
"type": "music",
|
||||||
|
"title": string,
|
||||||
|
"title_alt": string,
|
||||||
|
"artist": string,
|
||||||
|
"year": string,
|
||||||
|
"label": string,
|
||||||
|
"hyperlink": string,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CommentaryData {
|
||||||
|
"type": "commentary",
|
||||||
|
"text": string, // may contain VTT-compatible styling, as specified at https://www.w3.org/TR/webvtt1/#model-overview
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TranscriptData {
|
||||||
|
"type": "transcript",
|
||||||
|
"text": string, // may contain VTT-compatible styling, as specified at https://www.w3.org/TR/webvtt1/#model-overview
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VideoSourceData {
|
||||||
|
"type": "video_source",
|
||||||
|
"title": string,
|
||||||
|
"artist": string, // optional - may be empty string
|
||||||
|
"attribution": string,
|
||||||
|
"year": string,
|
||||||
|
"notes": string,
|
||||||
|
"retrieved_from": string,
|
||||||
|
"hyperlink": string,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NarrationSourceData {
|
||||||
|
"type": "narration_source",
|
||||||
|
"title": string,
|
||||||
|
"attribution": string,
|
||||||
|
"year": string,
|
||||||
|
"retrieval_date": string,
|
||||||
|
"source_type" : string, // PDF, etc
|
||||||
|
|
||||||
|
"hyperlink": string,
|
||||||
|
}
|
||||||
|
|
||||||
|
// adopting the OHMS standard http://ohda.matrix.msu.edu/2014/11/indexing-interviews-in-ohms/
|
||||||
|
interface OhmsData {
|
||||||
|
"type": "ohms",
|
||||||
|
"title": string,
|
||||||
|
"title_alt": string,
|
||||||
|
"synopsis": string,
|
||||||
|
"synopsis_alt": string,
|
||||||
|
"keywords": string,
|
||||||
|
"keywords_alt": string,
|
||||||
|
"subjects": string,
|
||||||
|
"subjects_alt": string,
|
||||||
|
"gpspoints": {
|
||||||
|
"gps": string,
|
||||||
|
"gps_zoom": string,
|
||||||
|
"gps_text": string,
|
||||||
|
"gps_text_alt": string
|
||||||
|
},
|
||||||
|
"hyperlinks": {
|
||||||
|
"hyperlink": string,
|
||||||
|
"hyperlink_text": string,
|
||||||
|
"hyperlink_text_alt": string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class MetadataPoint extends Component<MetadataPointProps, { isActive: boolean}> {
|
||||||
|
|
||||||
|
|
||||||
constructor(props: MetadataPointProps) {
|
constructor(props: MetadataPointProps) {
|
||||||
super(props)
|
super(props)
|
||||||
|
this.state = {
|
||||||
|
isActive: false
|
||||||
|
}
|
||||||
|
this.props.cue.onenter = this.onEnter.bind(this)
|
||||||
|
this.props.cue.onexit = this.onExit.bind(this)
|
||||||
this.onClick = this.onClick.bind(this)
|
this.onClick = this.onClick.bind(this)
|
||||||
|
// get current theme (but in a class component)
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const data = JSON.parse(this.props.cue.text)
|
let style = ''
|
||||||
const titleAlt = data.title_alt
|
if (this.state.isActive) {
|
||||||
? <h3 className="titleAlt">{data.title_alt}</h3>
|
// active
|
||||||
: ""
|
style = "bg-gray-200"
|
||||||
const synopsis = data.synopsis
|
}
|
||||||
? <div className="field">
|
// exect JSON.parse data to be of type MetadataPointData
|
||||||
<span>Synopsis</span>
|
const point = JSON.parse(this.props.cue.text) as MetadataPointData
|
||||||
{data.synopsis}
|
const data = point.data
|
||||||
</div>
|
|
||||||
: ""
|
let song = null
|
||||||
const synopsisAlt = data.synopsis_alt
|
let footage = null
|
||||||
? <div>{data.synopsis_alt}</div>
|
|
||||||
: ""
|
// get type of data
|
||||||
const keywords = data.keywords
|
if (point.type == "music") {
|
||||||
? <div className="field">
|
// const song is point.data as MusicData
|
||||||
<span>Keywords: </span>
|
song = data as MusicData
|
||||||
{data.keywords}
|
}
|
||||||
</div>
|
|
||||||
: ""
|
if (point.type == "video_source") {
|
||||||
const keywordsAlt = data.keywords_alt
|
footage = data as VideoSourceData
|
||||||
? <div className="field">
|
}
|
||||||
<span>Alternative Keywords: </span>
|
|
||||||
{data.keywords_alt}
|
|
||||||
</div>
|
|
||||||
: ""
|
|
||||||
const subjects = data.subjects
|
|
||||||
? <div className="field">
|
|
||||||
<span>Subjects: </span>
|
|
||||||
{data.subjects}
|
|
||||||
</div>
|
|
||||||
: ""
|
|
||||||
const subjectsAlt = data.subjects_alt
|
|
||||||
? <div className="field">
|
|
||||||
<span>Alternative Subjects: </span>
|
|
||||||
{data.subjects_alt}
|
|
||||||
</div>
|
|
||||||
: ""
|
|
||||||
const gpsLink = data.gpspoints.gps
|
|
||||||
? <div className="field">
|
|
||||||
<span>Geo: </span>
|
|
||||||
<a href={`https://www.google.com/maps/@?api=1&map_action=map¢er=${data.gpspoints.gps}&zoom=${data.gpspoints.gps_zoom}`}>{data.gpspoints.gps_text}</a>
|
|
||||||
</div>
|
|
||||||
: ""
|
|
||||||
const hyperlinks = data.hyperlinks.hyperlink_text
|
|
||||||
? <div className="field">
|
|
||||||
<span>Links: </span>
|
|
||||||
<a href={data.hyperlinks.hyperlink}>{data.hyperlinks.hyperlink_text}</a>
|
|
||||||
</div>
|
|
||||||
: ""
|
|
||||||
return (
|
return (
|
||||||
<div className="point">
|
<div className={`point ${style}`}>
|
||||||
<div className="time" onClick={this.onClick}>
|
<div className="time" onClick={this.onClick}>
|
||||||
[{this.startTime()}]
|
[{this.startTime()} - {this.endTime()}]
|
||||||
</div>
|
</div>
|
||||||
<div className="text">
|
<div className="text">
|
||||||
<h2 className="title" onClick={this.onClick}>{data.title}</h2>
|
{point.type == "music" &&
|
||||||
{titleAlt}
|
<div className="music" onClick={this.onClick}>
|
||||||
{synopsis}
|
<SongCard song={song} />
|
||||||
{synopsisAlt}
|
</div>
|
||||||
{keywords}
|
}
|
||||||
{keywordsAlt}
|
{point.type == "video_source" &&
|
||||||
{subjects}
|
<div className="footage" onClick={this.onClick}>
|
||||||
{subjectsAlt}
|
<VideoSourceCard videoSource={footage} />
|
||||||
{gpsLink}
|
</div>
|
||||||
{hyperlinks}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -94,6 +147,18 @@ class MetadataPoint extends Component<MetadataPointProps> {
|
||||||
return this.formatSeconds(this.props.cue.startTime)
|
return this.formatSeconds(this.props.cue.startTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
endTime() {
|
||||||
|
return this.formatSeconds(this.props.cue.endTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
onEnter() {
|
||||||
|
this.setState({isActive: true})
|
||||||
|
}
|
||||||
|
|
||||||
|
onExit() {
|
||||||
|
this.setState({isActive: false})
|
||||||
|
}
|
||||||
|
|
||||||
formatSeconds(t) {
|
formatSeconds(t) {
|
||||||
let mins = Math.floor(t / 60)
|
let mins = Math.floor(t / 60)
|
||||||
if (mins < 10) {
|
if (mins < 10) {
|
||||||
|
@ -110,4 +175,38 @@ class MetadataPoint extends Component<MetadataPointProps> {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const VideoSourceCard: React.FC<VideoSourceData> = ({videoSource}) => {
|
||||||
|
return (
|
||||||
|
<div className="bg-white shadow-md p-4">
|
||||||
|
<h2 className="text-lg font-bold">{videoSource.title}</h2>
|
||||||
|
<div className="text-gray-600">
|
||||||
|
<p className="mb-1">{videoSource.artist}</p>
|
||||||
|
<p className="mb-1">{videoSource.year}</p>
|
||||||
|
{/* render the html that may be inside of {videoSource.notes} */}
|
||||||
|
<div className="mb-1" dangerouslySetInnerHTML={{__html: videoSource.notes}} />
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
href={videoSource.hyperlink}
|
||||||
|
className="top-0 right-0 bottom-0 bg-gray-500 hover:bg-gray-400 text-white text-xs font-bold px-3 py-1"
|
||||||
|
>
|
||||||
|
Link
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: fix typing
|
||||||
|
const SongCard: React.FC<MusicData> = ({song}) => {
|
||||||
|
return (
|
||||||
|
<div className="bg-white shadow-md p-4">
|
||||||
|
<h2 className="text-lg font-bold">{song.title}</h2>
|
||||||
|
<div className="text-gray-600">
|
||||||
|
<p className="mb-1">{song.artist}</p>
|
||||||
|
<p className="mb-1">{song.year}</p>
|
||||||
|
<p className="mb-1">{song.label}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default MetadataPoint
|
export default MetadataPoint
|
|
@ -26,9 +26,16 @@ const Home: NextPage = () => {
|
||||||
toggle theme
|
toggle theme
|
||||||
</button>
|
</button>
|
||||||
<div className="container flex flex-col items-center justify-center gap-12 px-4 py-16 ">
|
<div className="container flex flex-col items-center justify-center gap-12 px-4 py-16 ">
|
||||||
<h1 className="text-1xl font-extrabold tracking-tight text-black dark:text-white sm:text-[3rem]">
|
{/* should hug upper-lefthand side */}
|
||||||
viewer
|
{/* text in the upper-lefthand corner with some padding */}
|
||||||
|
<div className="titles">
|
||||||
|
<h1 className="text-1xl font-extrabold tracking-tight text-black dark:text-white sm:text-[3rem] top-4 left-4 absolute">
|
||||||
|
documentary metadata viewer
|
||||||
</h1>
|
</h1>
|
||||||
|
{/* hug the left-hand side of the screen */}
|
||||||
|
<h2 className=" tracking-tight text-gray-500 dark:text-white top-20 left-6 absolute"><i>current film</i>: MIT: REGRESSIONS intro</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<Player />
|
<Player />
|
||||||
|
|
||||||
|
|
|
@ -69,12 +69,14 @@
|
||||||
/* TranscriptLine */
|
/* TranscriptLine */
|
||||||
|
|
||||||
.webvtt-player .active {
|
.webvtt-player .active {
|
||||||
background-color: #eee;
|
/* background-color: #eee; */
|
||||||
border: 1px solid #ccc;
|
/* light border */
|
||||||
|
@apply bg-gray-200 border-gray-600 border-2 border-opacity-10 border-x-0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.webvtt-player .match {
|
.webvtt-player .match {
|
||||||
background-color: lightyellow;
|
/* background-color: lightyellow; */
|
||||||
|
@apply bg-gray-200;
|
||||||
}
|
}
|
||||||
|
|
||||||
.webvtt-player .line {
|
.webvtt-player .line {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user