8. IIIF Assemble
8.1. About
Digital Initiatives maintains a standalone PHP application that generates presentation v3 manifests on the fly called iiif assemble.
This section describes this application and its relationship to the Rising from the Ashes project.
8.2. Why Assemble?
In early 2021, Digital Initiatives decided it could deliver the Rising from the Ashes project with IIIF. While we knew we could do this, we recognized that it would require developing a few pieces of technology. The first of those was IIIF assemble.
In order to serve audio and video with IIIF, presentation v3 is required. While there are a few projects in Islandora related to IIIF presentation, none of them are based on version 3 of the specification. For this reason, we needed an application that generated IIIF v3 presentation manifests for objects in Islandora 7. This was because we decided in early 2021, that it would be unacceptable for these files to be static. In order to address this problem, it was decided that the developers would build a IIIF assemble application using the lingua franca programming language of the group: PHP.
The current version of the software can be found in the departmental Github.
8.3. Running Assemble in Production
Assemble is installed on digital at /vhosts/digital/web/assemble.
Configuration of the application is done by the systems administrators.
The main branch is used in production.
The application can be updated with sudo git pull.
8.4. Running Assemble in Development
Assemble can be run in the utk_digital vagrant box. To do this, you must follow the instructions in the README of that repo.
There is one catch. Audio and Video content models are expected to have durations. You’ll need to add a duration with API-M to get this to work as expected. See the RELS-INT section of these docs for more information.
8.5. Assemble and RFTA
8.5.1. Audio and Video Manifests
IIIF assemble builds a Presentation v3 manifest for each object in RFTA. The metadata model follows the default for all other content except in a few ways.
Since this is timebased, the manifest has a duration property that is generated from an expected
bibframe:duration property in the RELS-INT that refers to the access datastream.
The other differences are described in corresponding sections below (structures and ranges).
8.5.2. Collection Manifests
Canopy, our platform for serving things, needs a presentation manifest for the collection object. This application builds that using its collection module.
8.5.3. The Metadata Property
Most metadata elements from MODS get mapped to the metadata property in our manifest. These are listed here:
Label |
Presentation v3 Field |
MODS XPath |
|---|---|---|
Alternative Title |
metadata.[i].label.en.0.”Alternative Title” |
titleInfo[@type=”alternative”]’ |
Table of Contents |
metadata.[i].label.en.0.”Table of Contents” |
tableOfContents |
Creators and Contributors |
metadata.[i].label.en.0.”Creators and Contributors” |
name/namePart |
Publisher |
metadata.[i].label.en.0.”Publisher” |
originInfo/publisher |
Date |
metadata.[i].label.en.0.”Date” |
originInfo/dateCreated | originInfo/dateOther |
Publication Date |
metadata.[i].label.en.0.”Publication Date” |
originInfo/dateIssued |
Format |
metadata.[i].label.en.0.”Format” |
physicalDescription/form[not(@type=”material”)] |
Extent |
metadata.[i].label.en.0.”Extent” |
physicalDescription/extent |
Subject |
metadata.[i].label.en.0.”Subject” |
subject[not(@displayLabel=”Narrator Class”)]/topic |
Narrator Role |
metadata.[i].label.en.0.”Narrator Role” |
subject[@displayLabel=”Narrator Class”]/topic |
Place |
metadata.[i].label.en.0.”Place” |
subject/geographic |
Time Period |
metadata.[i].label.en.0.”Time Period” |
subject/temporal |
Publication Identifier |
metadata.[i].label.en.0.”Publication Identifier” |
identifier[@type=”isbn”] | identifier[@type=”issn”] |
Description |
metadata.[i].label.en.0.”Description” |
abstract[not(@lang)] |
Descripción |
metadata.[i].label.es.0.”Descripción” |
abstract[@lang=”spa”] |
Título |
metadata.[i].label.es.0.”Título” |
titleInfo[@lang=”spa”]/title |
[Role of a Person to a Work] |
metadata.[i].label.en.0.[Role of a Person to a Work] |
mods:name[mods:role[mods:roleTerm[text()=’{$current}’]]]/mods:namePart |
Browse |
metadata.[i].label.en.0.Browse |
note[@displayLabel=”Browse”] |
Most of those are straight forward, but there are a few things to note.
Two elements get mapped with a Spanish language code rather than English:
Descripción
Título
We have many role terms that we use in our repository. In RFTA, currently we only have an Interviewer and an Interviewee. If this was to expand those names would be stored in a unique field according to the role of the person and their relationship to the work.
You can read more about how this is done in the Manifest module and the IIIF::buildMetadata() method of
iiif_assemble.
public function buildMetadata () {
$metadata = array(
'Alternative Title' => $this->xpath->query('titleInfo[@type="alternative"]'),
'Table of Contents' => $this->xpath->query('tableOfContents'),
'Publisher' => $this->xpath->query('originInfo/publisher'),
'Date' => $this->xpath->query('originInfo/dateCreated|originInfo/dateOther'),
'Publication Date' => $this->xpath->query('originInfo/dateIssued'),
'Format' => $this->xpath->query('physicalDescription/form[not(@type="material")]'),
'Extent' => $this->xpath->query('physicalDescription/extent'),
'Subject' => $this->xpath->query('subject[not(@displayLabel="Narrator Class")]/topic'),
'Narrator Role' => $this->xpath->query('subject[@displayLabel="Narrator Class"]/topic'),
'Place' => $this->xpath->query('subject/geographic'),
'Time Period' => $this->xpath->query('subject/temporal'),
'Publication Identifier' => $this->xpath->queryFilterByAttribute('identifier', false, 'type', ['issn','isbn'])
);
$metadata_with_names = $this->add_names_to_metadata($metadata);
return self::validateMetadata($metadata_with_names);
}
8.5.4. Other Properties
There are other metadata elements that are stored outside the metadata property. Those are:
Label |
Presentation v3 Field |
XPath |
|---|---|---|
Label |
label.en[0] |
titleInfo[not(@type=”alternative”)][not(@lang)] |
Summary |
summary.en[0] |
abstract[not(@lang)] |
Rights |
rights |
accessCondition[@xlink:href] |
Provided by |
requiredStatement.label.en[0] |
recordInfo/recordContentSource |
You can read more about this in the Manifest module and the IIIF::buildManifest() method of
iiif_assemble.
public function buildManifest ()
{
$id = $this->url . str_replace('?update=1', '', $_SERVER["REQUEST_URI"]);
$manifest['@context'] = ['https://iiif.io/api/presentation/3/context.json'];
$manifest['id'] = $id;
$manifest['type'] = 'Manifest';
$manifest['label'] = self::getLanguageArray($this->xpath->query('titleInfo[not(@type="alternative")]'), 'value');
$manifest['summary'] = self::getLanguageArray($this->xpath->query('abstract'), 'value');
$manifest['metadata'] = self::buildMetadata();
$manifest['rights'] = self::buildRights();
$manifest['requiredStatement'] = self::buildRequiredStatement();
$manifest['provider'] = self::buildProvider();
$manifest['thumbnail'] = self::buildThumbnail(200, 200);
$manifest['items'] = self::buildItems($id);
$manifest['seeAlso'] = self::buildSeeAlso();
if ($this->type === 'Book') {
$manifest['behavior'] = ["paged"];
}
$presentation = self::buildStructures($manifest, $id);
return json_encode($presentation);
}
8.6. Structures, Ranges, and Additional Expectations
The MODS record also includes a PBCore extension that is intended to be used to create navigable sections to the video. This is this section of the MODS:
<extension xmlns:pbcore="http://www.pbcore.org/PBCore/PBCoreNamespace.html"
xsi:schemaLocation="http://www.pbcore.org/PBCore/PBCoreNamespace.html https://raw.githubusercontent.com/WGBH/PBCore_2.1/master/pbcore-2.1.xsd">
<pbcore:pbcoreDescriptionDocument>
<pbcore:pbcoreIdentifier source="local">20200313_Schwartz_John-Reamer_Salley</pbcore:pbcoreIdentifier>
<pbcore:pbcoreTitle>Interview with John Schwartz and Salley Reamer, 2020-03-13</pbcore:pbcoreTitle>
<pbcore:pbcoreDescription>So, basically the soil horizons, the top layer of soil, what vegetation grows from and everything is the organic layer. And so at those high sites it wasn't even there because it had burned completely away. And so it can have, I'd say it has big impacts on vegetation returning.</pbcore:pbcoreDescription>
<pbcore:pbcorePart partType="Interview Questions"
startTime="00:00:53"
endTime="00:01:43">
<pbcore:pbcoreIdentifier source="local">20200313_Schwartz_John-Reamer_Salley_Q1</pbcore:pbcoreIdentifier>
<pbcore:pbcoreTitle>Can you state your name and relationship to the University of Tennessee?</pbcore:pbcoreTitle>
<pbcore:pbcoreDescription>Question 1</pbcore:pbcoreDescription>
</pbcore:pbcorePart>
<pbcore:pbcorePart partType="Interview Questions"
startTime="00:01:43"
endTime="00:02:12">
<pbcore:pbcoreIdentifier source="local">20200313_Schwartz_John-Reamer_Salley_Q2</pbcore:pbcoreIdentifier>
<pbcore:pbcoreTitle>Can you give a reason why you agreed to the interview?</pbcore:pbcoreTitle>
<pbcore:pbcoreDescription>Question 2</pbcore:pbcoreDescription>
</pbcore:pbcorePart>
<pbcore:pbcorePart partType="Interview Questions"
startTime="00:02:12"
endTime="00:02:47">
<pbcore:pbcoreIdentifier source="local">20200313_Schwartz_John-Reamer_Salley_Q3</pbcore:pbcoreIdentifier>
<pbcore:pbcoreTitle>Sally, how long have you been in the area?</pbcore:pbcoreTitle>
<pbcore:pbcoreDescription>Question 3</pbcore:pbcoreDescription>
</pbcore:pbcorePart>
<pbcore:pbcorePart partType="Interview Questions"
startTime="00:02:47"
endTime="00:04:37">
<pbcore:pbcoreIdentifier source="local">20200313_Schwartz_John-Reamer_Salley_Q4</pbcore:pbcoreIdentifier>
<pbcore:pbcoreTitle>When did each of you first become aware that there was a fire in the Smokies?</pbcore:pbcoreTitle>
<pbcore:pbcoreDescription>Question 4</pbcore:pbcoreDescription>
</pbcore:pbcorePart>
<pbcore:pbcorePart partType="Interview Questions"
startTime="00:04:37"
endTime="00:05:27">
<pbcore:pbcoreIdentifier source="local">20200313_Schwartz_John-Reamer_Salley_Q5</pbcore:pbcoreIdentifier>
<pbcore:pbcoreTitle>What was your reaction when you first heard that the fire had gotten out of control?</pbcore:pbcoreTitle>
<pbcore:pbcoreDescription>Question 5</pbcore:pbcoreDescription>
</pbcore:pbcorePart>
<pbcore:pbcorePart partType="Interview Questions"
startTime="00:05:27"
endTime="00:06:07">
<pbcore:pbcoreIdentifier source="local">20200313_Schwartz_John-Reamer_Salley_Q6</pbcore:pbcoreIdentifier>
<pbcore:pbcoreTitle>When you called your friend in Gatlinburg, what did you find out?</pbcore:pbcoreTitle>
<pbcore:pbcoreDescription>Question 6</pbcore:pbcoreDescription>
</pbcore:pbcorePart>
<pbcore:pbcorePart partType="Interview Questions"
startTime="00:06:07"
endTime="00:07:02">
<pbcore:pbcoreIdentifier source="local">20200313_Schwartz_John-Reamer_Salley_Q7</pbcore:pbcoreIdentifier>
<pbcore:pbcoreTitle>When did you first see the results of the fire?</pbcore:pbcoreTitle>
<pbcore:pbcoreDescription>Question 7</pbcore:pbcoreDescription>
</pbcore:pbcorePart>
<pbcore:pbcorePart partType="Interview Questions"
startTime="00:07:02"
endTime="00:09:48">
<pbcore:pbcoreIdentifier source="local">20200313_Schwartz_John-Reamer_Salley_Q8</pbcore:pbcoreIdentifier>
<pbcore:pbcoreTitle>Can you describe what your research in the park had been?</pbcore:pbcoreTitle>
<pbcore:pbcoreDescription>Question 8</pbcore:pbcoreDescription>
</pbcore:pbcorePart>
<pbcore:pbcorePart partType="Interview Questions"
startTime="00:09:48"
endTime="00:11:15">
<pbcore:pbcoreIdentifier source="local">20200313_Schwartz_John-Reamer_Salley_Q9</pbcore:pbcoreIdentifier>
<pbcore:pbcoreTitle>What was your role in the project and what was your interest?</pbcore:pbcoreTitle>
<pbcore:pbcoreDescription>Question 9</pbcore:pbcoreDescription>
</pbcore:pbcorePart>
<pbcore:pbcorePart partType="Interview Questions"
startTime="00:11:15"
endTime="00:12:19">
<pbcore:pbcoreIdentifier source="local">20200313_Schwartz_John-Reamer_Salley_Q10</pbcore:pbcoreIdentifier>
<pbcore:pbcoreTitle>What kind of impact from the fire were you looking for?</pbcore:pbcoreTitle>
<pbcore:pbcoreDescription>Question 10</pbcore:pbcoreDescription>
</pbcore:pbcorePart>
<pbcore:pbcorePart partType="Interview Questions"
startTime="00:12:19"
endTime="00:15:06">
<pbcore:pbcoreIdentifier source="local">20200313_Schwartz_John-Reamer_Salley_Q11</pbcore:pbcoreIdentifier>
<pbcore:pbcoreTitle>What did you hope to find studying the stream? Did the fire release sulfur?</pbcore:pbcoreTitle>
<pbcore:pbcoreDescription>Question 11</pbcore:pbcoreDescription>
</pbcore:pbcorePart>
<pbcore:pbcorePart partType="Interview Questions"
startTime="00:15:06"
endTime="00:16:06">
<pbcore:pbcoreIdentifier source="local">20200313_Schwartz_John-Reamer_Salley_Q12</pbcore:pbcoreIdentifier>
<pbcore:pbcoreTitle>Did you see any differences amongst the varying burn level sites?</pbcore:pbcoreTitle>
<pbcore:pbcoreDescription>Question 12</pbcore:pbcoreDescription>
</pbcore:pbcorePart>
<pbcore:pbcorePart partType="Interview Questions"
startTime="00:16:06"
endTime="00:16:49">
<pbcore:pbcoreIdentifier source="local">20200313_Schwartz_John-Reamer_Salley_Q13</pbcore:pbcoreIdentifier>
<pbcore:pbcoreTitle>Can you explain what organic content means?</pbcore:pbcoreTitle>
<pbcore:pbcoreDescription>Question 13</pbcore:pbcoreDescription>
</pbcore:pbcorePart>
<pbcore:pbcorePart partType="Interview Questions"
startTime="00:16:49"
endTime="00:17:22">
<pbcore:pbcoreIdentifier source="local">20200313_Schwartz_John-Reamer_Salley_Q14</pbcore:pbcoreIdentifier>
<pbcore:pbcoreTitle>What other differences did you see?</pbcore:pbcoreTitle>
<pbcore:pbcoreDescription>Question 14</pbcore:pbcoreDescription>
</pbcore:pbcorePart>
<pbcore:pbcorePart partType="Interview Questions"
startTime="00:17:22"
endTime="00:19:31">
<pbcore:pbcoreIdentifier source="local">20200313_Schwartz_John-Reamer_Salley_Q15</pbcore:pbcoreIdentifier>
<pbcore:pbcoreTitle>What distinctions are you seeing between this fire and ones out West?</pbcore:pbcoreTitle>
<pbcore:pbcoreDescription>Question 15</pbcore:pbcoreDescription>
</pbcore:pbcorePart>
<pbcore:pbcorePart partType="Interview Questions"
startTime="00:19:31"
endTime="00:20:25">
<pbcore:pbcoreIdentifier source="local">20200313_Schwartz_John-Reamer_Salley_Q16</pbcore:pbcoreIdentifier>
<pbcore:pbcoreTitle>Did the decrease in nitrogen surprise you?</pbcore:pbcoreTitle>
<pbcore:pbcoreDescription>Question 16</pbcore:pbcoreDescription>
</pbcore:pbcorePart>
<pbcore:pbcorePart partType="Interview Questions"
startTime="00:20:25"
endTime="00:21:25">
<pbcore:pbcoreIdentifier source="local">20200313_Schwartz_John-Reamer_Salley_Q17</pbcore:pbcoreIdentifier>
<pbcore:pbcoreTitle>What other hypotheses did you have going into this?</pbcore:pbcoreTitle>
<pbcore:pbcoreDescription>Question 17</pbcore:pbcoreDescription>
</pbcore:pbcorePart>
<pbcore:pbcorePart partType="Interview Questions"
startTime="00:21:25"
endTime="00:21:50">
<pbcore:pbcoreIdentifier source="local">20200313_Schwartz_John-Reamer_Salley_Q18</pbcore:pbcoreIdentifier>
<pbcore:pbcoreTitle>Why did you expect the fire to influence the streams?</pbcore:pbcoreTitle>
<pbcore:pbcoreDescription>Question 18</pbcore:pbcoreDescription>
</pbcore:pbcorePart>
<pbcore:pbcorePart partType="Interview Questions"
startTime="00:21:50"
endTime="00:22:48">
<pbcore:pbcoreIdentifier source="local">20200313_Schwartz_John-Reamer_Salley_Q19</pbcore:pbcoreIdentifier>
<pbcore:pbcoreTitle>Were there any other hypotheses you had?</pbcore:pbcoreTitle>
<pbcore:pbcoreDescription>Question 19</pbcore:pbcoreDescription>
</pbcore:pbcorePart>
<pbcore:pbcorePart partType="Interview Questions"
startTime="00:22:48"
endTime="00:23:51">
<pbcore:pbcoreIdentifier source="local">20200313_Schwartz_John-Reamer_Salley_Q20</pbcore:pbcoreIdentifier>
<pbcore:pbcoreTitle>Can you describe what the recovery was like and the severity?</pbcore:pbcoreTitle>
<pbcore:pbcoreDescription>Question 20</pbcore:pbcoreDescription>
</pbcore:pbcorePart>
<pbcore:pbcorePart partType="Interview Questions"
startTime="00:23:51"
endTime="00:25:12">
<pbcore:pbcoreIdentifier source="local">20200313_Schwartz_John-Reamer_Salley_Q21</pbcore:pbcoreIdentifier>
<pbcore:pbcoreTitle>Isn't a fire like this a natural occurrence?</pbcore:pbcoreTitle>
<pbcore:pbcoreDescription>Question 21</pbcore:pbcoreDescription>
</pbcore:pbcorePart>
<pbcore:pbcorePart partType="Interview Questions"
startTime="00:25:12"
endTime="00:26:54">
<pbcore:pbcoreIdentifier source="local">20200313_Schwartz_John-Reamer_Salley_Q22</pbcore:pbcoreIdentifier>
<pbcore:pbcoreTitle>What is the trajectory of your research? Where do you see it going?</pbcore:pbcoreTitle>
<pbcore:pbcoreDescription>Question 22</pbcore:pbcoreDescription>
</pbcore:pbcorePart>
<pbcore:pbcorePart partType="Interview Questions"
startTime="00:26:54"
endTime="00:27:37">
<pbcore:pbcoreIdentifier source="local">20200313_Schwartz_John-Reamer_Salley_Q23</pbcore:pbcoreIdentifier>
<pbcore:pbcoreTitle>Is this UT that's doing the long-term water monitoring project?</pbcore:pbcoreTitle>
<pbcore:pbcoreDescription>Question 23</pbcore:pbcoreDescription>
</pbcore:pbcorePart>
<pbcore:pbcorePart partType="Interview Questions"
startTime="00:27:37"
endTime="00:29:29">
<pbcore:pbcoreIdentifier source="local">20200313_Schwartz_John-Reamer_Salley_Q24</pbcore:pbcoreIdentifier>
<pbcore:pbcoreTitle>Is this UT that's doing the long-term water monitoring project?</pbcore:pbcoreTitle>
<pbcore:pbcoreDescription>Question 24</pbcore:pbcoreDescription>
</pbcore:pbcorePart>
<pbcore:pbcorePart partType="Interview Questions"
startTime="00:29:29"
endTime="00:30:25">
<pbcore:pbcoreIdentifier source="local">20200313_Schwartz_John-Reamer_Salley_Q25</pbcore:pbcoreIdentifier>
<pbcore:pbcoreTitle>Hae you run across any research stating that the water quality has affected other living things in the forest?</pbcore:pbcoreTitle>
<pbcore:pbcoreDescription>Question 25</pbcore:pbcoreDescription>
</pbcore:pbcorePart>
<pbcore:pbcorePart partType="Interview Questions"
startTime="00:30:25"
endTime="00:33:25">
<pbcore:pbcoreIdentifier source="local">20200313_Schwartz_John-Reamer_Salley_Q26</pbcore:pbcoreIdentifier>
<pbcore:pbcoreTitle>Was there anything from the fire to your research findings that was surprising?</pbcore:pbcoreTitle>
<pbcore:pbcoreDescription>Question 26</pbcore:pbcoreDescription>
</pbcore:pbcorePart>
<pbcore:pbcorePart partType="Interview Questions"
startTime="00:33:25"
endTime="00:34:20">
<pbcore:pbcoreIdentifier source="local">20200313_Schwartz_John-Reamer_Salley_Q27</pbcore:pbcoreIdentifier>
<pbcore:pbcoreTitle>Do you think another fire like this could happen?</pbcore:pbcoreTitle>
<pbcore:pbcoreDescription>Question 27</pbcore:pbcoreDescription>
</pbcore:pbcorePart>
<pbcore:pbcorePart partType="Interview Questions"
startTime="00:34:20"
endTime="00:36:28">
<pbcore:pbcoreIdentifier source="local">20200313_Schwartz_John-Reamer_Salley_Q28</pbcore:pbcoreIdentifier>
<pbcore:pbcoreTitle>Is there anything else that you would like to share?</pbcore:pbcoreTitle>
<pbcore:pbcoreDescription>Question 28</pbcore:pbcoreDescription>
</pbcore:pbcorePart>
</pbcore:pbcoreDescriptionDocument>
</extension>
There is a lot going on here, but this will explain the original intention so that we can discuss potential options.
First, it’s important to note that we set out to deliver this collection following the IIIF Presentation v3 specification.
The data team had gone threw each video and divided it into at least one, but sometimes two, ranges. Each range had sections that related to a timestamp or set of timestamps. To make use of the data creations team’s sectioning of videos, we planned to use structures and ranges.
To understand what we were attempting to do with this, let’s take a look at one of our sample IIIF manifests in the new version of Universal Viewer.
You can see that when you click the index, you have actionable anchors that forward you to the correct part of the video.
The same can be said about the Viewer in our application, canopy.
The interview questions section gets populated into our manifest like so:
"structures": [{
"type": "Range",
"id": "https://raw.githubusercontent.com/markpbaggett/utk_iiif_recipes/main/raw_manifests/rfta_video/range/1",
"label": {
"en": [
"Interview Questions"
]
},
This is informed by our MODS based on the pbcorePart[@partType] value:
<pbcore:pbcorePart partType="Interview Questions"
startTime="00:01:12"
endTime="00:03:09">
Each unique value of this attribute indicates that there should be a new range. This record only has one.
Similary, each of these nodes points out a new range in that range. For instance, the part of the video shown above goes into our manifest like this:
{
"type": "Range",
"id": "https://raw.githubusercontent.com/markpbaggett/utk_iiif_recipes/main/raw_manifests/rfta_video/range/1/question/4",
"label": {
"en": [
"When did each of you first become aware that there was a fire in the Smokies?"
]
},
"items": [{
"type": "Canvas",
"id": "https://raw.githubusercontent.com/markpbaggett/utk_iiif_recipes/main/raw_manifests/rfta_video/canvas#t=180,290"
}]
},
This is informed by our MODS in this section:
<pbcore:pbcorePart partType="Interview Questions"
startTime="00:02:47"
endTime="00:04:37">
<pbcore:pbcoreIdentifier source="local">20200313_Schwartz_John-Reamer_Salley_Q4</pbcore:pbcoreIdentifier>
<pbcore:pbcoreTitle>When did each of you first become aware that there was a fire in the Smokies?</pbcore:pbcoreTitle>
<pbcore:pbcoreDescription>Question 4</pbcore:pbcoreDescription>
</pbcore:pbcorePart>
Many of the values here are unused. The significant XPaths are:
pbcore:pbcorePart[@partType]which states the range it should belong to.pbcore:pbcoreTitlewhich states the title of the sub range.pbcore:pbcorePart[@startTime]which is a human readable start time that we convert on the fly to a W3C mediafragment.pbcore:pbcorePart[@endTime]which is a human readable end time that we convert on the fly to a W3C mediafragment.