Last active
April 1, 2020 09:32
-
-
Save RoadrunnerWMC/2eb52d44a5b03d4f22520eeb335e47dd to your computer and use it in GitHub Desktop.
The script behind Skawo's April Fools 2020 video (https://youtu.be/XCb3-nU9KZc)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# RRWMC, Mar 31 2020 | |
# The script behind Skawo's April Fools 2020 video | |
# (https://youtu.be/XCb3-nU9KZc) | |
# | |
# Download the source code for deeng from | |
# https://bisqwit.iki.fi/source/deeng.html | |
# Find the following in deeng.cc: | |
# else if(base == 'h' && res[a+1] == 'u') | |
# { | |
# base = 'f'; | |
# } | |
# and replace that middle line with "base = 'h';". | |
# Then compile it and place the binary in the same folder as this script. | |
# If on Windows, you may need to search for "./deeng" in this script and | |
# add a ".exe" to the end. Or not; I'm not sure. | |
import argparse | |
import json | |
import pathlib | |
import string | |
import subprocess | |
# Special thanks to Bent and Meatball132 for these exception tables | |
COMBINATIONAL_HIRIGANA = { | |
'kya': 'ki ya', | |
'kyu': 'ki yu', | |
'kyo': 'ki yo', | |
'gya': 'gi ya', | |
'gyu': 'gi yu', | |
'gyo': 'gi yo', | |
'sha': 'shi ya', | |
'shu': 'shi yu', | |
'sho': 'shi yo', | |
'jya': 'ji ya', | |
'jyu': 'ji yu', | |
'jyo': 'ji yo', | |
'cha': 'chi ya', | |
'chu': 'chi yu', | |
'cho': 'chi yo', | |
'dya': 'di ya', | |
'dyu': 'di yu', | |
'dyo': 'di yo', | |
'nya': 'na ya', | |
'nyu': 'na yu', | |
'nyo': 'na yo', | |
'hya': 'hi ya', | |
'hyu': 'hi yu', | |
'hyo': 'hi yo', | |
'bya': 'bi ya', | |
'byu': 'bi yu', | |
'byo': 'bi yo', | |
'pya': 'pi ya', | |
'pyu': 'pi yu', | |
'pyo': 'pi yo', | |
'mya': 'mi ya', | |
'myu': 'mi yu', | |
'myo': 'mi yo', | |
'rya': 'ri ya', | |
'ryu': 'ri yu', | |
'ryo': 'ri yo', | |
'zya': 'zi ya', | |
'zyu': 'zi yu', | |
'zyo': 'zi yo', | |
'vya': 'u o ya', # really "vu ya" | |
'vyu': 'u o yu', # really "vu yu" | |
'vyo': 'u o yo', # really "vu yo" | |
'sya': 'shi ya', | |
'syu': 'shi yu', | |
'syo': 'shi yo', | |
'shya': 'shi ya', | |
'shyu': 'shi yu', | |
'shyo': 'shi yo', | |
'ja': 'ji ya', | |
'ju': 'ji yu', | |
'jo': 'ji yo', | |
'tsya': 'tsu ya', | |
'tsyu': 'tsu yu', | |
'tsyo': 'tsu yo', | |
'chya': 'chi ya', | |
'chyu': 'chi yu', | |
'chyo': 'chi yo', | |
'cha': 'chi ya', | |
'chu': 'chi yu', | |
'cho': 'chi yo', | |
} | |
KATAKANA_EXCEPTIONS = { | |
'fa': 'fu a', | |
'fi': 'fu i', | |
'fe': 'fu e', | |
'fo': 'fu o', | |
'she': 'shi e', | |
'je': 'ji e', | |
'che': 'chi e', | |
'tu': 'to u', | |
'ty': 'te i', | |
'du': 'do u', | |
'dy': 'de i', | |
'tsa': 'tsu a', | |
'tsi': 'tsu i', | |
'tse': 'tsu e', | |
'tso': 'tsu o', | |
'va': 'u a', | |
'bwa': 'u a', | |
'vi': 'u i', | |
'bwi': 'u i', | |
'bu': 'u', | |
've': 'u e', | |
'bwe': 'u e', | |
'vo': 'u o', | |
'vu': 'u o', | |
} | |
OTHER_RULES = { | |
'f': 'fu', | |
'tchi': 'tsu chi', | |
'tshi': 'tsu shi', | |
} | |
# http://www.kasuto.net/wind_waker.php?main=wind_waker/font.html | |
HYLIAN_FONT_CHARACTERS = { | |
'a': 'A', | |
'i': 'B', | |
'u': 'C', | |
'e': 'D', | |
'o': 'E', | |
'ka': 'F', | |
'ki': 'G', | |
'ku': 'H', | |
'ke': 'I', | |
'ko': 'J', | |
'sa': '+', | |
'si': 'K', | |
'shi': 'K', | |
'su': 'L', | |
'se': 'M', | |
'so': 'N', | |
'ta': 'O', | |
'ti': 'P', | |
'chi': 'P', | |
'tu': 'Q', | |
'tsu': 'Q', | |
'te': 'R', | |
'to': 'S', | |
'na': 'T', | |
'ni': 'U', | |
'nu': 'V', | |
'ne': 'W', | |
'no': 'X', | |
'ha': 'Y', | |
'hi': 'Z', | |
'hu': 'a', | |
'fu': 'a', | |
'he': 'b', | |
'ho': 'c', | |
'ma': 'd', | |
'mi': 'e', | |
'mu': 'f', | |
'me': 'g', | |
'mo': 'h', | |
'ya': 'i', | |
'yu': 'j', | |
'yo': 'k', | |
'ra': 'l', | |
'ri': 'm', | |
'ru': 'n', | |
're': 'o', | |
'ro': 'p', | |
'wa': 'q', | |
'wo': 'r', | |
'n': 's', | |
'ga': 't', | |
'gi': 'u', | |
'gu': 'v', | |
'ge': 'w', | |
'go': 'x', | |
'za': 'y', | |
'zi': 'z', | |
'ji': 'z', | |
'zu': '{', | |
'ze': '}', | |
'zo': ';', | |
'da': ':', | |
'di': '>', | |
'ji': '>', | |
'du': '<', | |
'zu': '<', | |
'de': '?', | |
'do': '/', | |
'ba': '!', | |
'bi': '@', | |
'bu': '#', | |
'be': '$', | |
'bo': '%', | |
'pa': '&', | |
'pi': '=', | |
'pu': '*', | |
'pe': '(', | |
'po': ')', | |
'1': '1', | |
'2': '2', | |
'3': '3', | |
'4': '4', | |
'5': '5', | |
'6': '6', | |
'7': '7', | |
'8': '8', | |
'9': '9', | |
'0': '0', | |
} | |
MASTER_TABLE = dict(HYLIAN_FONT_CHARACTERS) | |
for combo, expanded in {**COMBINATIONAL_HIRIGANA, **KATAKANA_EXCEPTIONS, **OTHER_RULES}.items(): | |
expanded = expanded.split(' ') | |
MASTER_TABLE[combo] = ''.join(MASTER_TABLE[x] for x in expanded) | |
MASTER_TABLE_1 = {a: b for a, b in MASTER_TABLE.items() if len(a) == 1} | |
MASTER_TABLE_2 = {a: b for a, b in MASTER_TABLE.items() if len(a) == 2} | |
MASTER_TABLE_3 = {a: b for a, b in MASTER_TABLE.items() if len(a) == 3} | |
MASTER_TABLE_4 = {a: b for a, b in MASTER_TABLE.items() if len(a) == 4} | |
MASTER_TABLE_REVERSE = {b: a for a, b in MASTER_TABLE.items()} | |
def deeng(s): | |
""" | |
Run the given string through deeng | |
""" | |
return subprocess.run(['./deeng', s], stdout=subprocess.PIPE).stdout.decode('utf-8').rstrip() | |
def deeng_separately(s): | |
""" | |
Run each word of the string through deeng separately, preserving | |
punctuation | |
""" | |
currentWord = [] | |
overallOutput = [] | |
for c in s: | |
if c in string.ascii_letters: | |
currentWord.append(c) | |
else: | |
if currentWord: | |
overallOutput.append(deeng(''.join(currentWord))) | |
currentWord.clear() | |
overallOutput.append(c) | |
if currentWord: | |
overallOutput.append(deeng(''.join(currentWord))) | |
return ''.join(overallOutput) | |
def getFirstSyllable(s): | |
""" | |
Get the first syllable in s, and return (syllable, fontchar) | |
""" | |
# double-consonant exception | |
if len(s) >= 2 and s[0] == s[1] and s[0] in (set(string.ascii_letters) - set('aeiou')): | |
return s[0], MASTER_TABLE['tsu'] | |
if len(s) >= 4 and s[:4] in MASTER_TABLE_4: | |
return s[:4], MASTER_TABLE_4[s[:4]] | |
if len(s) >= 3 and s[:3] in MASTER_TABLE_3: | |
return s[:3], MASTER_TABLE_3[s[:3]] | |
if len(s) >= 2 and s[:2] in MASTER_TABLE_2: | |
return s[:2], MASTER_TABLE_2[s[:2]] | |
if len(s) >= 1 and s[:1] in MASTER_TABLE_1: | |
return s[:1], MASTER_TABLE_1[s[:1]] | |
raise ValueError('Cannot get syllable from start of ' + s) | |
def toHylian(s): | |
""" | |
Convert deeng output to Hylian-font-compatible text. | |
Output: | |
- string showing recognized syllables separated by spaces (for debugging) | |
- Hylian-font text | |
- list of indices of actual punctuation that should be rendered in a different font | |
""" | |
# Expand '^'s | |
out = [] | |
for c in s: | |
if c == '^': | |
out.append({ | |
'a': 'a', | |
'e': 'e', | |
'i': 'i', | |
'o': 'u', | |
'u': 'u', | |
}[out[-1]]) | |
else: | |
out.append(c) | |
s = ''.join(out) | |
# Convert to syllables | |
out1 = [] | |
out2 = [] | |
punctuationIndices = [] | |
i = 0 | |
while i < len(s): | |
if s[i].lower() not in string.ascii_letters + '^': | |
if s[i] not in ' \n' + string.digits: | |
punctuationIndices.append(len(''.join(out2))) | |
out1.append(s[i]) | |
out2.append(s[i]) | |
i += 1 | |
continue | |
syllable, char = getFirstSyllable(s[i:]) | |
i += len(syllable) | |
out1.append(syllable + ' ') | |
out2.append(char) | |
return ''.join(out1), ''.join(out2), punctuationIndices | |
def test(): | |
""" | |
Run some tests | |
""" | |
TEST_SENTENCES = [ | |
'and then something replaces the japanization with the hylian font keystrokes', | |
'also reading that stuff without the smaller versions of the characters is probably a nightmare but nothing we can do ig', | |
'nice', | |
'this other font uses punctuation for actual punctuation, but then the rest of the symbols is nonsense', | |
] | |
for s in TEST_SENTENCES: | |
print() | |
print(s) | |
# Run sentence through deeng | |
deenged = deeng_separately(s) | |
print(deenged) | |
# Convert to syllables and font-compatible characters | |
syllables, encrypted, _ = toHylian(deenged) | |
print(syllables) | |
print(encrypted) | |
wordlist = [] | |
with open('/usr/share/dict/american-english', 'r', encoding='utf-8') as f: | |
for line in f: | |
s = line.strip() | |
print() | |
print(s) | |
# Run sentence through deeng | |
deenged = deeng_separately(s) | |
print(deenged) | |
# Convert to syllables and font-compatible characters | |
syllables, encrypted, _ = toHylian(deenged) | |
print(syllables) | |
print(encrypted) | |
def modifyTscprojTextbox(box): | |
""" | |
Modify a tscproj textbox dictionary | |
""" | |
text = box['text'].replace('\r', '\n') | |
print() | |
print(text) | |
text = deeng_separately(text) | |
print(text) | |
text2 = text | |
for A, B in {'a^': 'ā', 'e^': 'ē', 'i^': 'ī', 'o^': 'ō', 'u^': 'ū'}.items(): | |
text2 = text2.replace(A, B) | |
print(text2) | |
syllables, text, punctuationIndices = toHylian(text) | |
print(syllables) | |
print(text) | |
print(punctuationIndices) | |
def makeKeyframe(name, start, stop, value, type): | |
return { | |
'name': name, | |
'rangeEnd': stop, | |
'rangeStart': start, | |
'value': value, | |
'valueType': type, | |
} | |
fontInfo = [ | |
makeKeyframe('underline', 0, len(text), 0, 'int'), | |
makeKeyframe('kerning', 0, len(text), 0, 'double'), | |
makeKeyframe('fontWeight', 0, len(text), 400, 'int'), | |
makeKeyframe('fgColor', 0, len(text), '(255,255,255,255)', 'color'), | |
makeKeyframe('strikethrough', 0, len(text), 0, 'int'), | |
makeKeyframe('fontItalic', 0, len(text), 0, 'int'), | |
] | |
HYLIAN = ('Wind Waker Hylian', 42) | |
PUNCTUATION = ('Ancient Hylian - English', 68) | |
currentFontStart = 0 | |
currentFont = HYLIAN | |
for i, c in enumerate(text): | |
if i in punctuationIndices: | |
newFont = PUNCTUATION | |
else: | |
newFont = HYLIAN | |
if currentFont != newFont: | |
fontInfo.append(makeKeyframe('fontName', currentFontStart, i, currentFont[0], 'string')) | |
fontInfo.append(makeKeyframe('fontSize', currentFontStart, i, currentFont[1], 'double')) | |
currentFont = newFont | |
currentFontStart = i | |
fontInfo.append(makeKeyframe('fontName', currentFontStart, len(text), currentFont[0], 'string')) | |
fontInfo.append(makeKeyframe('fontSize', currentFontStart, len(text), currentFont[1], 'double')) | |
box['text'] = text.replace('\n', '\r') | |
box['textAttributes']['keyframes'] = [{'endTime': 0, 'time': 0, 'value': fontInfo, 'duration': 0}] | |
def main(args=None): | |
parser = argparse.ArgumentParser( | |
description='Hylian text thingy') | |
subparsers = parser.add_subparsers(title='commands', | |
description='(run a command with -h for additional help)') | |
def handleString(pArgs): | |
""" | |
Handle the "string" command. | |
""" | |
deenged = deeng_separately(pArgs.string) | |
print(deenged) | |
print() | |
syllables, encrypted, _ = toHylian(deenged) | |
print(syllables) | |
print() | |
print(encrypted) | |
parser_string = subparsers.add_parser('string', aliases=['s'], | |
help='convert a single string') | |
parser_string.add_argument('string', | |
help='string to convert (quoted)') | |
parser_string.set_defaults(func=handleString) | |
def handleTscproj(pArgs): | |
""" | |
Handle the "tscproj" command. | |
""" | |
outfp = pArgs.output_file | |
if outfp is None: outfp = pArgs.input_file.with_suffix('.out.tscproj') | |
with open(pArgs.input_file, 'r', encoding='utf-8') as f: | |
j = json.load(f) | |
allMedias = [] | |
for scene in j['timeline']['sceneTrack']['scenes']: | |
for track in scene['csml']['tracks']: | |
for media in track['medias']: | |
if media['_type'] == 'Callout': | |
allMedias.append(media) | |
for media in sorted(allMedias, key=lambda m: m.get('start', 0)): | |
modifyTscprojTextbox(media['def']) | |
with open(str(outfp), 'w', encoding='utf-8') as f: | |
json.dump(j, f) | |
parser_tscproj = subparsers.add_parser('tscproj', aliases=['t'], | |
help='apply Hylian to a tscproj file') | |
parser_tscproj.add_argument('input_file', type=pathlib.Path, | |
help='input file to process') | |
parser_tscproj.add_argument('output_file', nargs='?', type=pathlib.Path, | |
help='what to save the output file as') | |
parser_tscproj.set_defaults(func=handleTscproj) | |
# Parse args and run appropriate function | |
pArgs = parser.parse_args(args) | |
if hasattr(pArgs, 'func'): | |
pArgs.func(pArgs) | |
else: # this happens if no arguments were specified at all | |
parser.print_usage() | |
# test() | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment