Changes to wolf and MV

This commit is contained in:
DazedAnon 2024-09-27 11:01:39 -05:00
parent dd472cb89f
commit 1ed64f2e56
4 changed files with 169 additions and 109 deletions

View file

@ -192,7 +192,7 @@ def getImageDimensions(file_path):
def processImagesDir(directory_path, imageList): def processImagesDir(directory_path, imageList):
for file_name in os.listdir(directory_path): for file_name in os.listdir(directory_path):
# .png and Japanese # .png and Japanese
if '.png' in file_name and re.search(r'[一-龠ぁ-ゔァ-ヴー]+', file_name) and file_name.replace('.png', '') in VOCAB: if '.png' in file_name and file_name.replace('.png', '') in VOCAB:
file_path = os.path.join(directory_path, file_name) file_path = os.path.join(directory_path, file_name)
if os.path.isfile(file_path): if os.path.isfile(file_path):
# Check if the file is an image # Check if the file is an image

View file

@ -60,11 +60,11 @@ LEAVE = False
# Dialogue / Scroll # Dialogue / Scroll
CODE401 = True CODE401 = True
CODE405 = False CODE405 = True
CODE408 = False CODE408 = False
# Choices # Choices
CODE102 = False CODE102 = True
# Variables # Variables
CODE122 = False CODE122 = False
@ -488,7 +488,10 @@ def searchNames(data, pbar, context):
tokensResponse = translateNote(data[i], r'<note:(.*?)>') tokensResponse = translateNote(data[i], r'<note:(.*?)>')
totalTokens[0] += tokensResponse[0] totalTokens[0] += tokensResponse[0]
totalTokens[1] += tokensResponse[1] totalTokens[1] += tokensResponse[1]
if 'PE拡張' in data[i]['note']:
tokensResponse = translateNote(data[i], r'<PE拡張:(.*?)>')
totalTokens[0] += tokensResponse[0]
totalTokens[1] += tokensResponse[1]
i += 1 i += 1
else: else:
batchFull = True batchFull = True
@ -611,7 +614,9 @@ def searchNames(data, pbar, context):
else: else:
# Get Text # Get Text
if data[j]['name'] != '': if data[j]['name'] != '':
data[j]['name'] = translatedNameBatch[0] with open('translations.txt', 'a', encoding='utf-8') as file:
file.write(f'{data[j]['name']} ({translatedNameBatch[0]})\n')
data[j]['name'] = translatedNameBatch[0]
translatedNameBatch.pop(0) translatedNameBatch.pop(0)
if data[j]['nickname'] != '': if data[j]['nickname'] != '':
data[j]['nickname'] = translatedNicknameBatch[0] data[j]['nickname'] = translatedNicknameBatch[0]
@ -644,26 +649,29 @@ def searchNames(data, pbar, context):
# Set Data # Set Data
if len(nameList) == len(translatedNameBatch): if len(nameList) == len(translatedNameBatch):
j = k j = k
while j < i: with open('translations.txt', 'a', encoding='utf-8') as file:
# Empty Data file.write(f'# Items\n')
if data[j] is None or data[j]['name'] == "": while j < i:
j += 1 # Empty Data
continue if data[j] is None or data[j]['name'] == "":
else: j += 1
# Get Text continue
data[j]['name'] = translatedNameBatch[0] else:
if 'description' in data[j]: # Get Text
data[j]['description'] = textwrap.fill(translatedDescriptionBatch[0], LISTWIDTH) file.write(f'{data[j]['name']} ({translatedNameBatch[0]})\n')
translatedNameBatch.pop(0) data[j]['name'] = translatedNameBatch[0]
translatedDescriptionBatch.pop(0) if 'description' in data[j]:
data[j]['description'] = textwrap.fill(translatedDescriptionBatch[0], LISTWIDTH)
translatedNameBatch.pop(0)
translatedDescriptionBatch.pop(0)
# If Batch is empty. Move on. # If Batch is empty. Move on.
if len(translatedNameBatch) == 0: if len(translatedNameBatch) == 0:
nameList.clear() nameList.clear()
descriptionList.clear() descriptionList.clear()
batchFull = False batchFull = False
filling = False filling = False
j += 1 j += 1
else: else:
mismatch = True mismatch = True
if context in ['Enemies', 'Classes', 'MapInfos']: if context in ['Enemies', 'Classes', 'MapInfos']:
@ -720,14 +728,12 @@ def searchCodes(page, pbar, jobList, filename):
list355655 = [] list355655 = []
list108 = [] list108 = []
setData = False setData = False
currentGroup = []
textHistory = [] textHistory = []
match = [] match = []
totalTokens = [0, 0] totalTokens = [0, 0]
translatedText = '' translatedText = ''
speaker = '' speaker = ''
speakerID = None speakerID = None
nametag = ''
syncIndex = 0 syncIndex = 0
CLFlag = False CLFlag = False
maxHistory = MAXHISTORY maxHistory = MAXHISTORY
@ -761,6 +767,10 @@ def searchCodes(page, pbar, jobList, filename):
if len(codeList) <= i: if len(codeList) <= i:
break break
# Declare Varss
currentGroup = []
nametag = ''
## Event Code: 401 Show Text ## Event Code: 401 Show Text
if 'code' in codeList[i] and codeList[i]['code'] in [401, 405, -1] and (CODE401 or CODE405): if 'code' in codeList[i] and codeList[i]['code'] in [401, 405, -1] and (CODE401 or CODE405):
# Save Code and starting index (j) # Save Code and starting index (j)
@ -810,8 +820,15 @@ def searchCodes(page, pbar, jobList, filename):
if len(speakerList) == 0: if len(speakerList) == 0:
speakerList = re.findall(r'^[\\]+[cC]\[[\d]+\](.+?)[\\]+[Cc]\[[\d]\]\\?\\?$', jaString) speakerList = re.findall(r'^[\\]+[cC]\[[\d]+\](.+?)[\\]+[Cc]\[[\d]\]\\?\\?$', jaString)
# None # First Line Speakers
if len(speakerList) == 0 and FIRSTLINESPEAKERS is True: if len(speakerList) == 0 and FIRSTLINESPEAKERS is True:
# Remove any RPGMaker Code at start
ffMatch = re.search(r'^(\s*[\\]+[aAbBdDeEfFgGhHiIjJlLmMoOpPqQrRsStTuUvVwWxXyYzZ]+\[[\w\d\[\]\\]+\])', jaString)
if ffMatch != None:
jaString = jaString.replace(ffMatch.group(0), '')
nametag += ffMatch.group(0)
# Test Speaker
if len(jaString) < 40 \ if len(jaString) < 40 \
and 'code' in codeList[i+1] \ and 'code' in codeList[i+1] \
and codeList[i+1]['code'] in [401, 405, -1] \ and codeList[i+1]['code'] in [401, 405, -1] \
@ -828,7 +845,8 @@ def searchCodes(page, pbar, jobList, filename):
totalTokens[1] += response[1][1] totalTokens[1] += response[1][1]
# Set Data # Set Data
codeList[i]['parameters'][0] = jaString.replace(speakerList[0], speaker) codeList[i]['parameters'][0] = nametag + jaString.replace(speakerList[0], speaker)
nametag = ''
# Iterate to next string # Iterate to next string
i += 1 i += 1
@ -861,7 +879,8 @@ def searchCodes(page, pbar, jobList, filename):
# Format String # Format String
if len(currentGroup) > 0: if len(currentGroup) > 0:
finalJAString = ' '.join(currentGroup).replace('', '?') finalJAString = ''
finalJAString = ''.join(currentGroup).replace('', '?')
oldjaString = finalJAString oldjaString = finalJAString
# Check if Empty # Check if Empty
@ -956,7 +975,7 @@ def searchCodes(page, pbar, jobList, filename):
finalJAString = finalJAString.replace(match, '') finalJAString = finalJAString.replace(match, '')
# Remove any RPGMaker Code at start # Remove any RPGMaker Code at start
ffMatch = re.search(r'^([.\\]+[aAbBcCdDeEfFgGhHiIjJlLmMoOpPqQrRsStTuUvVwWxXyYzZ]+\[.+?\]\]?)+', finalJAString) ffMatch = re.search(r'^(\s*[\\]+[aAbBdDeEfFgGhHiIjJlLmMoOpPqQrRsStTuUvVwWxXyYzZ]+\[[\w\d\[\]\\]+\])', finalJAString)
if ffMatch != None: if ffMatch != None:
finalJAString = finalJAString.replace(ffMatch.group(0), '') finalJAString = finalJAString.replace(ffMatch.group(0), '')
nametag += ffMatch.group(0) nametag += ffMatch.group(0)
@ -997,6 +1016,7 @@ def searchCodes(page, pbar, jobList, filename):
list401.append(speaker) list401.append(speaker)
speaker = '' speaker = ''
match = [] match = []
nametag = ''
currentGroup = [] currentGroup = []
syncIndex = i + 1 syncIndex = i + 1
@ -2136,20 +2156,6 @@ def batchList(input_list, batch_size):
return [input_list[i:i + batch_size] for i in range(0, len(input_list), batch_size)] return [input_list[i:i + batch_size] for i in range(0, len(input_list), batch_size)]
def createContext(fullPromptFlag, subbedT, format): def createContext(fullPromptFlag, subbedT, format):
characters = 'Game Characters:\n\
圭太 (Keita) - Male\n\
涼香 (Ryoka) - Female\n\
(Saki) - Female\n\
大介 (Daisuke) - Male\n\
浮浪者 (Vagrant) - Male\n\
まさる (Masaru) - Male\n\
ノブオ (Nobuo) - Male\n\
安井 (Yasui) - Male\n\
大山 (Oyama) - Male\n\
関口 (Sekiguchi) - Male\n\
黒崎 (Kurosaki) - Male\n\
'
system = PROMPT + VOCAB if fullPromptFlag else \ system = PROMPT + VOCAB if fullPromptFlag else \
f"\ f"\
You are an expert Eroge Game translator who translates Japanese text to {LANGUAGE}.\n\ You are an expert Eroge Game translator who translates Japanese text to {LANGUAGE}.\n\
@ -2167,15 +2173,12 @@ Output ONLY the {LANGUAGE} translation in the following format: `Translation: <{
user = f'```json\n{subbedT}\n```' user = f'```json\n{subbedT}\n```'
else: else:
user = subbedT user = subbedT
return characters, system, user return system, user
def translateText(characters, system, user, history, penalty, format, model=MODEL): def translateText(system, user, history, penalty, format, model=MODEL):
# Prompt # Prompt
msg = [{"role": "system", "content": system}] msg = [{"role": "system", "content": system}]
# Characters
msg.append({"role": "system", "content": characters})
# History # History
if isinstance(history, list): if isinstance(history, list):
msg.extend([{"role": "system", "content": h} for h in history]) msg.extend([{"role": "system", "content": h} for h in history])
@ -2250,7 +2253,7 @@ def extractTranslation(translatedTextList, is_list):
return None return None
def countTokens(characters, system, user, history): def countTokens(system, user, history):
inputTotalTokens = 0 inputTotalTokens = 0
outputTotalTokens = 0 outputTotalTokens = 0
enc = tiktoken.encoding_for_model('gpt-4') enc = tiktoken.encoding_for_model('gpt-4')
@ -2262,7 +2265,6 @@ def countTokens(characters, system, user, history):
else: else:
inputTotalTokens += len(enc.encode(history)) inputTotalTokens += len(enc.encode(history))
inputTotalTokens += len(enc.encode(system)) inputTotalTokens += len(enc.encode(system))
inputTotalTokens += len(enc.encode(characters))
inputTotalTokens += len(enc.encode(user)) inputTotalTokens += len(enc.encode(user))
# Output # Output
@ -2306,17 +2308,17 @@ def translateGPT(text, history, fullPromptFlag):
continue continue
# Create Message # Create Message
characters, system, user = createContext(fullPromptFlag, subbedT, format) system, user = createContext(fullPromptFlag, subbedT, format)
# Calculate Estimate # Calculate Estimate
if ESTIMATE: if ESTIMATE:
estimate = countTokens(characters, system, user, history) estimate = countTokens(system, user, history)
totalTokens[0] += estimate[0] totalTokens[0] += estimate[0]
totalTokens[1] += estimate[1] totalTokens[1] += estimate[1]
continue continue
# Translating # Translating
response = translateText(characters, system, user, history, 0.05, format) response = translateText(system, user, history, 0.05, format)
translatedText = response.choices[0].message.content translatedText = response.choices[0].message.content
totalTokens[0] += response.usage.prompt_tokens totalTokens[0] += response.usage.prompt_tokens
totalTokens[1] += response.usage.completion_tokens totalTokens[1] += response.usage.completion_tokens
@ -2327,7 +2329,7 @@ def translateGPT(text, history, fullPromptFlag):
extractedTranslations = extractTranslation(translatedText, True) extractedTranslations = extractTranslation(translatedText, True)
if extractedTranslations == None or len(tItem) != len(extractedTranslations): if extractedTranslations == None or len(tItem) != len(extractedTranslations):
# Mismatch. Try Again # Mismatch. Try Again
response = translateText(characters, system, user, history, 0.05, format, 'gpt-4o') response = translateText(system, user, history, 0.05, format, 'gpt-4o')
translatedText = response.choices[0].message.content translatedText = response.choices[0].message.content
totalTokens[0] += response.usage.prompt_tokens totalTokens[0] += response.usage.prompt_tokens
totalTokens[1] += response.usage.completion_tokens totalTokens[1] += response.usage.completion_tokens

View file

@ -62,7 +62,7 @@ FILENAME = None
# Dialogue / Scroll # Dialogue / Scroll
CODE101 = False CODE101 = False
CODE102 = False CODE102 = False
CODE122 = False CODE122 = True
# Other # Other
CODE210 = False CODE210 = False
@ -468,9 +468,10 @@ def searchCodes(events, pbar, jobList, filename):
codeList[i]['stringArgs'][0] = translatedText codeList[i]['stringArgs'][0] = translatedText
### Event Code: 300 Common Events ### Event Code: 300 Common Events
if codeList[i]['code'] == 300 and CODE300 == True and len(codeList[i]['stringArgs']) > 0: if codeList[i]['code'] == 300 and CODE300 == True and 'stringArgs' in codeList[i] and len(codeList[i]['stringArgs']) > 1:
# Choices # Choices
if codeList[i]['stringArgs'][0] == "[共]汎用ウィンドウ生成": if codeList[i]['stringArgs'][0] == "[共]汎用ウィンドウ生成" \
or codeList[i]['stringArgs'][0] == "[共]選択生成":
# Grab String # Grab String
choiceList = codeList[i]['stringArgs'][1].split('\r\n') choiceList = codeList[i]['stringArgs'][1].split('\r\n')
@ -502,54 +503,56 @@ def searchCodes(events, pbar, jobList, filename):
elif codeList[i]['stringArgs'][0] == "Hメッセージ" \ elif codeList[i]['stringArgs'][0] == "Hメッセージ" \
or codeList[i]['stringArgs'][0] == 'Hしらべる' \ or codeList[i]['stringArgs'][0] == 'Hしらべる' \
or codeList[i]['stringArgs'][0] == '[共]Hメッセージ+' \ or codeList[i]['stringArgs'][0] == '[共]Hメッセージ+' \
or codeList[i]['stringArgs'][0] == '[共]ポップアップ表示' \ or codeList[i]['stringArgs'][0] == 'd[共]ポップアップ表示' \
or codeList[i]['stringArgs'][0] == 'm_謎冒頭' \ or codeList[i]['stringArgs'][0] == 'm_謎冒頭' \
or (codeList[i]['codeStr'] == 'SetString' and '\n' in codeList[i]['stringArgs'][0])\ or (codeList[i]['codeStr'] == 'SetString' and ('' in codeList[i]['stringArgs'][0] or '*' in codeList[i]['stringArgs'][0]))\
or codeList[i]['stringArgs'][0] == '[移]サウンドノベル': or codeList[i]['stringArgs'][0] == '[移]サウンドノベル':
if len(codeList[i]['stringArgs']) > 1: cleanedList = None
if len(codeList[i]['stringArgs']) > 1 and not re.search(r'^[\\]+cself\[\d+\]$', codeList[i]['stringArgs'][1]):
cleanedList = formatDramon(codeList[i]['stringArgs'][1]) cleanedList = formatDramon(codeList[i]['stringArgs'][1])
else: elif codeList[i]['code'] == 122:
cleanedList = formatDramon(codeList[i]['stringArgs'][0]) cleanedList = formatDramon(codeList[i]['stringArgs'][0])
fontSize = 24 if cleanedList:
translatedText = '' fontSize = 24
translatedText = ''
for str in cleanedList:
# Pass 1 for str in cleanedList:
if not setData: # Pass 1
if all(x not in str for x in ['_', '@', '>', '/']) and str != '\r\n': if not setData:
# Remove Textwrap and Font and Add to list if all(x not in str for x in ['_', '@', '>', '/']) and str != '\r\n':
str = str.replace('\r\n', ' ') # Remove Textwrap and Font and Add to list
str = str.replace(f'\\f[{fontSize}]', '') str = str.replace('\r\n', ' ')
list300.append(str) str = str.replace(f'\\f[{fontSize}]', '')
list300.append(str)
# Pass 2 # Pass 2
else:
if all(x not in str for x in ['_', '@', '>', '/',]) and str != '\r\n':
# Decide Wrap
if codeList[i]['stringArgs'][0] == '[移]サウンドノベル':
width = 40
else:
width = WIDTH
# Add Textwrap and Font
list300[0] = textwrap.fill(list300[0], width)
list300[0] = list300[0].replace('\n', f'\r\n\\f[{fontSize}]')
list300[0] = f'\\f[{fontSize}]{list300[0]}\r\n'
translatedText += list300[0]
list300.pop(0)
else: else:
translatedText += str if all(x not in str for x in ['_', '@', '>', '/',]) and str != '\r\n':
# Decide Wrap
if codeList[i]['stringArgs'][0] == '[移]サウンドノベル':
width = 40
else:
width = WIDTH
# Write to File # Add Textwrap and Font
if setData: list300[0] = textwrap.fill(list300[0], width)
# Formatting Fixes list300[0] = list300[0].replace('\n', f'\r\n\\f[{fontSize}]')
translatedText = translatedText.replace('*"', '* "') list300[0] = f'\\f[{fontSize}]{list300[0]}\r\n'
translatedText = translatedText.replace('\r\n\r\n', '\r\n') translatedText += list300[0]
translatedText = re.sub(r'[^\S\r\n]+', ' ', translatedText) list300.pop(0)
if len(codeList[i]['stringArgs']) > 1: else:
codeList[i]['stringArgs'][1] = translatedText translatedText += str
else:
codeList[i]['stringArgs'][0] = translatedText # Write to File
if setData:
# Formatting Fixes
translatedText = translatedText.replace('*"', '* "')
translatedText = translatedText.replace('\r\n\r\n', '\r\n')
translatedText = re.sub(r'[^\S\r\n]+', ' ', translatedText)
if len(codeList[i]['stringArgs']) > 1:
codeList[i]['stringArgs'][1] = translatedText
else:
codeList[i]['stringArgs'][0] = translatedText
### Event Code: 250 Common Events ### Event Code: 250 Common Events
if codeList[i]['code'] == 250 and CODE250 == True: if codeList[i]['code'] == 250 and CODE250 == True:
@ -820,7 +823,7 @@ def searchDB(events, pbar, jobList, filename):
scenarioList[2].pop(0) scenarioList[2].pop(0)
# Grab Items # Grab Items
if table['name'] == 'm_モンスター用名前候補' and ITEMFLAG == True: if table['name'] == 'オーブ' and ITEMFLAG == True:
with open('translations.txt', 'a', encoding='utf-8') as file: with open('translations.txt', 'a', encoding='utf-8') as file:
for item in table['data']: for item in table['data']:
dataList = item['data'] dataList = item['data']
@ -828,7 +831,7 @@ def searchDB(events, pbar, jobList, filename):
# Parse # # Parse #
for j in range(len(dataList)): for j in range(len(dataList)):
# Name # Name
if dataList[j].get('name') == '名前': if dataList[j].get('name') == 'オーブの名前':
# Pass 1 (Grab Data) # Pass 1 (Grab Data)
if setData == False: if setData == False:
if dataList[j].get('value') != '': if dataList[j].get('value') != '':
@ -842,9 +845,9 @@ def searchDB(events, pbar, jobList, filename):
itemList[0].pop(0) itemList[0].pop(0)
# Description 1 (You are my specialz) # Description 1 (You are my specialz)
if dataList[j].get('name') == 'NULL': if dataList[j].get('name') == 'オーブの説明':
# Clean String # Clean String
fontSize = 24 fontSize = 18
translatedText = '' translatedText = ''
cleanedList = formatDramon(dataList[j].get('value')) cleanedList = formatDramon(dataList[j].get('value'))
for str in cleanedList: for str in cleanedList:
@ -864,9 +867,9 @@ def searchDB(events, pbar, jobList, filename):
# Add Textwrap and Font # Add Textwrap and Font
tempText = itemList[1][0] tempText = itemList[1][0]
# tempText = textwrap.fill(tempText, width) tempText = textwrap.fill(tempText, width)
# tempText = tempText.replace('\n', f'\r\n\\f[{fontSize}]') tempText = tempText.replace('\n', f'\r\n\\f[{fontSize}]')
# tempText = f'\\f[{fontSize}]{tempText}\r\n' tempText = f'\\f[{fontSize}]{tempText}\r\n'
translatedText += tempText translatedText += tempText
itemList[1].pop(0) itemList[1].pop(0)
else: else:
@ -903,9 +906,9 @@ def searchDB(events, pbar, jobList, filename):
# Add Textwrap and Font # Add Textwrap and Font
tempText = itemList[2][0] tempText = itemList[2][0]
# tempText = textwrap.fill(tempText, width) tempText = textwrap.fill(tempText, width)
# tempText = tempText.replace('\n', f'\r\n\\f[{fontSize}]') tempText = tempText.replace('\n', f'\r\n\\f[{fontSize}]')
# tempText = f'\\f[{fontSize}]{tempText}\r\n' tempText = f'\\f[{fontSize}]{tempText}\r\n'
translatedText += tempText translatedText += tempText
itemList[2].pop(0) itemList[2].pop(0)
else: else:

View file

@ -1,5 +1,54 @@
Here are some vocabulary and terms so that you know the proper spelling and translation. Here are some vocabulary and terms so that you know the proper spelling and translation.
``` ```
# Game Characters
朱雀 (Suzuku) - Female
紅 (Akane) - Female
エージェントシリウス (Agent Sirius) - Female
白虎 (Shiratora) - Female
撫子 (Nadeshiko) - Female
鬼塚 (Onizuka) - Female
百合花 (Yurika) - Female
犬飼 (Inukai) - Female
アンナ (Anna) - Female
メープル (Maple) - Female
鈴懸 (Suzukake) - Female
いるか (Iruka) - Female
安知理 (Ajiri) - Female
結 (Yuu) - Female
綾香 (Ayaka) - Female
鳥居 (Tori) - Female
鈴蘭 (Suzuran) - Female
宇宙 (Sora) - Female
青龍 (Aotatsu) - Female
桔梗 (Kikyo) - Female
獅堂 (Shidou) - Female
向日葵 (Himawari) - Female
大紫 (Omurasaki) - Female
菫 (Sumire) - Female
亀神 (Kamegami) - Female
柊 (Shu) - Female
鷲尾 (Washio) - Male
菊一 (Kikuichi) - Male
花村 (Hanamura) - Female
咲夜 (Sakuya) - Female
曽我井 (Sogai) - Male
四海 (Shikai) - Male
志倉 綿 (Shikura Wata) - Female
皐月 亜麻 (Satsuki Ama) - Female
安知理 結 (Ajiri Yuu) - Female
栗山 五十木 (Kuriyama Itsuki) - Male
天狐 橙 (Tenkou Daidai) - Female
天狐 蜜柑 (Tenkou Mikan) - Female
鷹本 椿 (Takamoto Tsubaki) - Female
青龍 桃葉 (Aotatsu Momoba) - Female
雨宮 星羅 (Amamiya Seira) - Female
山内 紫苑 (Yamauchi Shion) - Female
野沢 林檎 (Nozawa Ringo) - Female
城ヶ峰 柳 (Jougamine Yanagi) - Female
山角 翠 (Yamakaku Midori) - Female
来杉馬来 (Kisugiba Rai) - Male
狼煙蓮 (Noroshi Ren) - Male
# Lewd Terms # Lewd Terms
マンコ (pussy) マンコ (pussy)
おまんこ (vagina) おまんこ (vagina)
@ -67,4 +116,10 @@ ME 音量 (ME Volume)
堕天使 (Fallen Angel) 堕天使 (Fallen Angel)
鬼 (Oni) 鬼 (Oni)
ローバー (Roper) ローバー (Roper)
# Skills
Highest Repeat Rate No.1 Girl
Veteran Courtesan
Vital Point Thrust
Majikite
``` ```