~ihabunek/vampires

ebc2dd26327f15c996b0f90758ba7d77769bd168 — Ivan Habunek 7 months ago
Initial commit
5 files changed, 284 insertions(+), 0 deletions(-)

A .gitignore
A LICENSE
A README.md
A index.html
A parse.py
A  => .gitignore +5 -0
@@ 1,5 @@
/audio
/fizer.swf
/fizer.xml
/slides
/tmp

A  => LICENSE +19 -0
@@ 1,19 @@
Copyright © 2021 Ivan Habunek <ivan@habunek.com>

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the “Software”), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
\ No newline at end of file

A  => README.md +34 -0
@@ 1,34 @@
# Vampires

## Prerequisites

* python3
* [swfmill](https://www.swfmill.org/)

## Instructions

Download the original swf presentation and save it in the root of this project.

```
curl https://rifters.com/blindsight/Fizer.swf -o fizer.swf
```

Extract the XML representation using swfmill

```
swfmill swf2xml fizer.swf > fizer.xml
```

Extract audio and images

```
python3 parse.py fizer.xml
```

Start a server:

```
python3 -m http.server 8080
```

Visit http://localhost:8080/ and enjoy

A  => index.html +143 -0
@@ 1,143 @@
<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Taming Yesterday's Nightmares for a Better Tomorrow</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta http-equiv="content-type" content="text/html; charset=utf-8">

    <style type="text/css">
      body { max-width: 1024px; margin: auto; font-family: sans-serif }
      footer { font-size: 0.9rem; margin-top: 5rem }
      .redacted { background-color: black; color: white; display: inline-block; }
      .strike { text-decoration: line-through }
      .centered { text-align: center }
      .player { width: 640px }
      .w-full { width: 100% }
      #audio { margin-top: 1rem }
      #slide { height: 640px }
    </style>
  </head>

  <body>
    <main>
      <h1>
        <span class="strike"> Vampires: Biology and Evolution</span>
        <span class="redacted">Redacted</span>
      </h1>

      <p>In light of recent events at the Melbourne Vatworks and elsewhere, the
      information on this page has been excised for reasons of planetary
      security. In its place, we offer this archival presentation on the
      discovery and resurrection of Homo sapiens whedonum from Fizerpharm Inc.,
      entitled "Taming Yesterday's Nightmares for a Better Tomorrow". This
      production has been vetted for the public domain, and in no way infringes
      upon corporate copyright. <span class="strike">Flash plugin</span>
      Javascript required.</p>

      <div class="centered">
        <img id="slide" src="slides/slide_00.jpg" />
      </div>

      <p><span id="current">1</span> / <span id="total"></span></p>

      <audio id="audio" class="w-full" controls src="audio/full.mp3">
        Your browser does not support the &lt;audio&gt; element, sorry.
      </audio>
    </main>

    <footer>
      <p>Extracted from original Flash video by Peter Watts:<br />
        <a href="https://rifters.com/blindsight/vampires.htm">https://rifters.com/blindsight/vampires.htm</a>
      </p>
      <p>Extraction code by Ivan Habunek available at:<br />
        <a href="https://sr.ht/~ihabunek/vampires/">https://sr.ht/~ihabunek/vampires/</a>
      </p>
    </footer>
  </body>

  <script type="text/javascript">
    const audio = document.querySelector("audio")

    // TODO: timings need tweaking
    const slideTimings = [
      0.0,     // 1
      25.9,    // 2
      51.3,    // 3
      110.3,   // 4
      125.4,   // 5
      156.0,   // 6
      231.6,   // 7
      268.7,   // 8
      294.3,   // 9
      307.6,   // 10
      318.1,   // 11
      333.1,   // 12
      344.7,   // 13
      356.3,   // 14
      374.3,   // 15
      388.1,   // 16
      403.1,   // 17
      449.7,   // 18
      486.1,   // 19
      542.1,   // 20
      567.7,   // 21
      631.4,   // 22
      705.7,   // 23
      749.6,   // 24
      830.6,   // 25
      844.7,   // 26
      860.7,   // 27
      913.4,   // 28
      921.4,   // 29
      928.0,   // 30
      969.4,   // 31
      1018.7,  // 32
      1082.3,  // 33
      1129.1,  // 34
      1140.0,  // 35
      1150.7,  // 36
      1161.1,  // 37
      1269.4,  // 38
      1330.6,  // 39
      1356.6,  // 40
      1389.9,  // 41
      1423.1,  // 42
      1500.6,  // 43
      1587.6,  // 44
      1595.6,  // 45
      1607.7,  // 46
      1622.7,  // 47
      1643.3,  // 48
      1695.1,  // 49
      1729.6,  // 50
      1754.1,  // 51
      1794.6,  // 52
      1862.6,  // 53
      1890.0,  // 54
      1964.7,  // 55
      2006.3,  // 56
      2074.3,  // 57
      2135.4,  // 58
    ]

    document.getElementById("total").innerText = slideTimings.length

    const getSlide = (timestamp) => {
      const index =  slideTimings.findIndex(slideTimestamp => {
        return slideTimestamp > timestamp
      })

      return index == -1 ? slideTimings.length - 1 : index
    }

    const slide = document.getElementById("slide")
    const current = document.getElementById("current")

    audio.addEventListener("timeupdate", event => {
      const index = getSlide(event.target.currentTime)
      const src = index < 10 ? `slides/slide_0${index}.jpg` : `slides/slide_${index}.jpg`
      slide.src = src
      current.innerText = index
    });
  </script>
</html>

A  => parse.py +83 -0
@@ 1,83 @@
#!/usr/bin/env python3

import sys
from lxml import etree
from base64 import b64decode


def extract_slides(root):
    for index, node in enumerate(root.findall(".//DefineBitsJPEG2")):
        data = node.find("data").find("data").text
        filename = f"slides/slide_{index:0>2}.jpg"
        print(f"Saving: {filename}")

        with open(filename, "wb") as f:
            f.write(b64decode(data))


def _extract_audio_segments(root):
    """Extract and yield audio from concurrent SoundStreamBlocks"""
    data = b""
    count = 0

    parent = root.find(".//SoundStreamBlock").getparent()
    for node in parent.findall("./*"):
        if node.tag == "ShowFrame":
            continue
        if node.tag == "SoundStreamBlock":
            block = b64decode(node.find("data").text)
            data += block[4:]  # remove 16bit header
            count += 1
        elif data:
            yield data, count
            data = b""
            count = 0


def _expand_audio_segments(root):
    """Join segments of 7 frames or smaller into the previous segment"""
    buffer = None
    buffer_count = 0

    for data, count in _extract_audio_segments(root):
        if buffer and count > 7:
            yield buffer, buffer_count
            buffer = data
            buffer_count = count
        elif buffer and count <= 7:
            buffer += data
            buffer_count += count
        else:
            buffer = data
            buffer_count = count

    if buffer:
        yield buffer, buffer_count


def extract_segmented_audio(root):
    total = 0
    all_data = b""
    for index, (data, count) in enumerate(_expand_audio_segments(root)):
        all_data += data
        minutes = int((total / 7) / 60)
        seconds = int(total / 7) - minutes * 60
        time = f"{minutes:0>2}:{seconds:0>2}"
        total += count
        print(f"Audio chunk @ {time} - {count} frames")

        # # Uncomment to save individual audio chunks
        # filename = f"audio/{index:0>2}_{time}_{count}.mp3"
        # print(filename)
        # with open(filename, "wb") as f:
        #     f.write(data)

    print("Saving: audio/full.mp3")
    with open("audio/full.mp3", "wb") as f:
        f.write(all_data)


with open(sys.argv[1], "r") as f:
    root = etree.parse(f).getroot()
    extract_slides(root)
    extract_segmented_audio(root)