working prototype of metadata viewer with defined schema

This commit is contained in:
dukeeagle 2022-12-31 17:27:13 -06:00
parent 4709d32be0
commit 46fd752f40
5 changed files with 251 additions and 74 deletions

View File

@ -2,8 +2,80 @@ WEBVTT
Kind: captions
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
{"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"
}
}

View File

@ -14,9 +14,6 @@ export default function Player() {
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
preload={false}
audio={audioUrl}

View File

@ -2,85 +2,138 @@ import React, { Component } from 'react'
// import './MetadataPoint.css'
type MetadataPointProps = {
cue: VTTCue
active: boolean
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) {
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)
// get current theme (but in a class component)
}
render() {
const data = JSON.parse(this.props.cue.text)
const titleAlt = data.title_alt
? <h3 className="titleAlt">{data.title_alt}</h3>
: ""
const synopsis = data.synopsis
? <div className="field">
<span>Synopsis</span>
{data.synopsis}
</div>
: ""
const synopsisAlt = data.synopsis_alt
? <div>{data.synopsis_alt}</div>
: ""
const keywords = data.keywords
? <div className="field">
<span>Keywords: </span>
{data.keywords}
</div>
: ""
const keywordsAlt = data.keywords_alt
? <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&center=${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>
: ""
let style = ''
if (this.state.isActive) {
// active
style = "bg-gray-200"
}
// exect JSON.parse data to be of type MetadataPointData
const point = JSON.parse(this.props.cue.text) as MetadataPointData
const data = point.data
let song = null
let footage = null
// get type of data
if (point.type == "music") {
// const song is point.data as MusicData
song = data as MusicData
}
if (point.type == "video_source") {
footage = data as VideoSourceData
}
return (
<div className="point">
<div className={`point ${style}`}>
<div className="time" onClick={this.onClick}>
[{this.startTime()}]
[{this.startTime()} - {this.endTime()}]
</div>
<div className="text">
<h2 className="title" onClick={this.onClick}>{data.title}</h2>
{titleAlt}
{synopsis}
{synopsisAlt}
{keywords}
{keywordsAlt}
{subjects}
{subjectsAlt}
{gpsLink}
{hyperlinks}
{point.type == "music" &&
<div className="music" onClick={this.onClick}>
<SongCard song={song} />
</div>
}
{point.type == "video_source" &&
<div className="footage" onClick={this.onClick}>
<VideoSourceCard videoSource={footage} />
</div>
}
</div>
</div>
)
@ -94,6 +147,18 @@ class MetadataPoint extends Component<MetadataPointProps> {
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) {
let mins = Math.floor(t / 60)
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

View File

@ -26,9 +26,16 @@ const Home: NextPage = () => {
toggle theme
</button>
<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]">
viewer
</h1>
{/* should hug upper-lefthand side */}
{/* 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>
{/* 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 />

View File

@ -69,12 +69,14 @@
/* TranscriptLine */
.webvtt-player .active {
background-color: #eee;
border: 1px solid #ccc;
/* background-color: #eee; */
/* light border */
@apply bg-gray-200 border-gray-600 border-2 border-opacity-10 border-x-0;
}
.webvtt-player .match {
background-color: lightyellow;
/* background-color: lightyellow; */
@apply bg-gray-200;
}
.webvtt-player .line {