add Composition Page (RWKV-Music)

This commit is contained in:
josc146 2023-07-28 12:30:05 +08:00
parent d0fd480bd6
commit daabcf58a0
20 changed files with 2565 additions and 144 deletions

View File

@ -0,0 +1,116 @@
# https://github.com/magenta/magenta-js/issues/164
import json
import os
import urllib.request
def get_pitches_array(min_pitch, max_pitch):
return list(range(min_pitch, max_pitch + 1))
base_url = 'https://storage.googleapis.com/magentadata/js/soundfonts'
soundfont_path = 'sgm_plus'
soundfont_json_url = f"{base_url}/{soundfont_path}/soundfont.json"
# Download soundfont.json
soundfont_json = ""
if not os.path.exists('soundfont.json'):
try:
with urllib.request.urlopen(soundfont_json_url) as response:
soundfont_json = response.read()
# Save soundfont.json
with open('soundfont.json', 'wb') as file:
file.write(soundfont_json)
except:
print("Failed to download soundfont.json")
else:
# If file exists, get it from the file system
with open('soundfont.json', 'rb') as file:
soundfont_json = file.read()
# Parse soundfont.json
soundfont_data = json.loads(soundfont_json)
if soundfont_data is not None:
# Iterate over each instrument
for instrument_id, instrument_name in soundfont_data['instruments'].items():
if not os.path.isdir(instrument_name):
# Create instrument directory if it doesn't exist
os.makedirs(instrument_name)
instrument_json = ""
instrument_path = f"{soundfont_path}/{instrument_name}"
if not os.path.exists(f"{instrument_name}/instrument.json"):
# Download instrument.json
instrument_json_url = f"{base_url}/{instrument_path}/instrument.json"
try:
with urllib.request.urlopen(instrument_json_url) as response:
instrument_json = response.read()
# Save instrument.json
with open(f"{instrument_name}/instrument.json", 'wb') as file:
file.write(instrument_json)
except:
print(f"Failed to download {instrument_name}/instrument.json")
else:
# If file exists, get it from the file system
with open(f"{instrument_name}/instrument.json", 'rb') as file:
instrument_json = file.read()
# Parse instrument.json
instrument_data = json.loads(instrument_json)
if instrument_data is not None:
# Iterate over each pitch and velocity
for velocity in instrument_data['velocities']:
pitches = get_pitches_array(instrument_data['minPitch'], instrument_data['maxPitch'])
for pitch in pitches:
# Create the file name
file_name = f'p{pitch}_v{velocity}.mp3'
# Check if the file already exists
if os.path.exists(f"{instrument_name}/{file_name}"):
pass
#print(f"Skipping {instrument_name}/{file_name} - File already exists")
else:
# Download pitch/velocity file
file_url = f"{base_url}/{instrument_path}/{file_name}"
try:
with urllib.request.urlopen(file_url) as response:
file_contents = response.read()
# Save pitch/velocity file
with open(f"{instrument_name}/{file_name}", 'wb') as file:
file.write(file_contents)
print(f"Downloaded {instrument_name}/{file_name}")
except:
print(f"Failed to download {instrument_name}/{file_name}")
else:
print(f"Failed to parse instrument.json for {instrument_name}")
else:
print('Failed to parse soundfont.json')

View File

@ -0,0 +1,134 @@
{
"name": "sgm_plus",
"instruments": {
"0": "acoustic_grand_piano",
"1": "bright_acoustic_piano",
"2": "electric_grand_piano",
"3": "honkytonk_piano",
"4": "electric_piano_1",
"5": "electric_piano_2",
"6": "harpsichord",
"7": "clavichord",
"8": "celesta",
"9": "glockenspiel",
"10": "music_box",
"11": "vibraphone",
"12": "marimba",
"13": "xylophone",
"14": "tubular_bells",
"15": "dulcimer",
"16": "drawbar_organ",
"17": "percussive_organ",
"18": "rock_organ",
"19": "church_organ",
"20": "reed_organ",
"21": "accordion",
"22": "harmonica",
"23": "tango_accordion",
"24": "acoustic_guitar_nylon",
"25": "acoustic_guitar_steel",
"26": "electric_guitar_jazz",
"27": "electric_guitar_clean",
"28": "electric_guitar_muted",
"29": "overdriven_guitar",
"30": "distortion_guitar",
"31": "guitar_harmonics",
"32": "acoustic_bass",
"33": "electric_bass_finger",
"34": "electric_bass_pick",
"35": "fretless_bass",
"36": "slap_bass_1",
"37": "slap_bass_2",
"38": "synth_bass_1",
"39": "synth_bass_2",
"40": "violin",
"41": "viola",
"42": "cello",
"43": "contrabass",
"44": "tremolo_strings",
"45": "pizzicato_strings",
"46": "orchestral_harp",
"47": "timpani",
"48": "string_ensemble_1",
"49": "string_ensemble_2",
"50": "synthstrings_1",
"51": "synthstrings_2",
"52": "choir_aahs",
"53": "voice_oohs",
"54": "synth_voice",
"55": "orchestra_hit",
"56": "trumpet",
"57": "trombone",
"58": "tuba",
"59": "muted_trumpet",
"60": "french_horn",
"61": "brass_section",
"62": "synthbrass_1",
"63": "synthbrass_2",
"64": "soprano_sax",
"65": "alto_sax",
"66": "tenor_sax",
"67": "baritone_sax",
"68": "oboe",
"69": "english_horn",
"70": "bassoon",
"71": "clarinet",
"72": "piccolo",
"73": "flute",
"74": "recorder",
"75": "pan_flute",
"76": "blown_bottle",
"77": "shakuhachi",
"78": "whistle",
"79": "ocarina",
"80": "lead_1_square",
"81": "lead_2_sawtooth",
"82": "lead_3_calliope",
"83": "lead_4_chiff",
"84": "lead_5_charang",
"85": "lead_6_voice",
"86": "lead_7_fifths",
"87": "lead_8_bass_lead",
"88": "pad_1_new_age",
"89": "pad_2_warm",
"90": "pad_3_polysynth",
"91": "pad_4_choir",
"92": "pad_5_bowed",
"93": "pad_6_metallic",
"94": "pad_7_halo",
"95": "pad_8_sweep",
"96": "fx_1_rain",
"97": "fx_2_soundtrack",
"98": "fx_3_crystal",
"99": "fx_4_atmosphere",
"100": "fx_5_brightness",
"101": "fx_6_goblins",
"102": "fx_7_echoes",
"103": "fx_8_scifi",
"104": "sitar",
"105": "banjo",
"106": "shamisen",
"107": "koto",
"108": "kalimba",
"109": "bag_pipe",
"110": "fiddle",
"111": "shanai",
"112": "tinkle_bell",
"113": "agogo",
"114": "steel_drums",
"115": "woodblock",
"116": "taiko_drum",
"117": "melodic_tom",
"118": "synth_drum",
"119": "reverse_cymbal",
"120": "guitar_fret_noise",
"121": "breath_noise",
"122": "seashore",
"123": "bird_tweet",
"124": "telephone_ring",
"125": "helicopter",
"126": "applause",
"127": "gunshot",
"drums": "percussion"
}
}

469
assets/soundfont_builder.rb Normal file
View File

@ -0,0 +1,469 @@
#!/usr/bin/env ruby
#
# JavaScript Soundfont Builder for MIDI.js
# Author: 0xFE <mohit@muthanna.com>
# edited by Valentijn Nieman <valentijnnieman@gmail.com>
#
# Requires:
#
# FluidSynth
# Lame
# Ruby Gems: midilib parallel
#
# $ brew install fluidsynth lame (on OSX)
# $ gem install midilib parallel
#
# You'll need to download a GM soundbank to generate audio.
#
# Usage:
#
# 1) Install the above dependencies.
# 2) Edit BUILD_DIR, SOUNDFONT, and INSTRUMENTS as required.
# 3) Run without any argument.
require 'base64'
require 'digest/sha1'
require 'etc'
require 'fileutils'
require 'midilib'
require 'parallel'
require 'zlib'
require 'json'
include FileUtils
BUILD_DIR = "./sound-font" # Output path
SOUNDFONT = "./default_sound_font.sf2" # Soundfont file path
# This script will generate MIDI.js-compatible instrument JS files for
# all instruments in the below array. Add or remove as necessary.
INSTRUMENTS = [
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
17,
18,
19,
20,
21,
22,
23,
24,
25,
26,
27,
28,
29,
30,
31,
32,
33,
34,
35,
36,
37,
38,
39,
40,
41,
42,
43,
44,
45,
46,
47,
48,
49,
50,
51,
52,
53,
54,
55,
56,
57,
58,
59,
60,
61,
62,
63,
64,
65,
66,
67,
68,
69,
70,
71,
72,
73,
74,
75,
76,
77,
78,
79,
80,
81,
82,
83,
84,
85,
86,
87,
88,
89,
90,
91,
92,
93,
94,
95,
96,
97,
98,
99,
100,
101,
102,
103,
104,
105,
106,
107,
108,
109,
110,
111,
112,
113,
114,
115,
116,
117,
118,
119,
120,
121,
122,
123,
124,
125,
126,
127
]
# It was found that midilib uses names that are incompatible with MIDI.js
# For example, midilib uses "SynthBrass 1" -> https://github.com/jimm/midilib/blob/6c8e481ae72cd9f00a38eb3700ddfca6b549f153/lib/midilib/consts.rb#L280
# and the MIDI association uses "SynthBrass 1" -> https://www.midi.org/specifications-old/item/gm-level-1-sound-set
# but the MIDI.js calls this "Synth Brass 1" -> https://github.com/mudcube/MIDI.js/blob/a8a84257afa70721ae462448048a87301fc1554a/js/midi/gm.js#L44
# there are others like "Bag pipe" vs "Bagpipe", etc.
# here, we use the MIDI.js definitions because that is how most users will interact with the generated soundfonts.
MIDIJS_PATCH_NAMES = [
"Acoustic Grand Piano",
"Bright Acoustic Piano",
"Electric Grand Piano",
"Honky-tonk Piano",
"Electric Piano 1",
"Electric Piano 2",
"Harpsichord",
"Clavinet",
"Celesta",
"Glockenspiel",
"Music Box",
"Vibraphone",
"Marimba",
"Xylophone",
"Tubular Bells",
"Dulcimer",
"Drawbar Organ",
"Percussive Organ",
"Rock Organ",
"Church Organ",
"Reed Organ",
"Accordion",
"Harmonica",
"Tango Accordion",
"Acoustic Guitar (nylon)",
"Acoustic Guitar (steel)",
"Electric Guitar (jazz)",
"Electric Guitar (clean)",
"Electric Guitar (muted)",
"Overdriven Guitar",
"Distortion Guitar",
"Guitar Harmonics",
"Acoustic Bass",
"Electric Bass (finger)",
"Electric Bass (pick)",
"Fretless Bass",
"Slap Bass 1",
"Slap Bass 2",
"Synth Bass 1",
"Synth Bass 2",
"Violin",
"Viola",
"Cello",
"Contrabass",
"Tremolo Strings",
"Pizzicato Strings",
"Orchestral Harp",
"Timpani",
"String Ensemble 1",
"String Ensemble 2",
"Synth Strings 1",
"Synth Strings 2",
"Choir Aahs",
"Voice Oohs",
"Synth Choir",
"Orchestra Hit",
"Trumpet",
"Trombone",
"Tuba",
"Muted Trumpet",
"French Horn",
"Brass Section",
"Synth Brass 1",
"Synth Brass 2",
"Soprano Sax",
"Alto Sax",
"Tenor Sax",
"Baritone Sax",
"Oboe",
"English Horn",
"Bassoon",
"Clarinet",
"Piccolo",
"Flute",
"Recorder",
"Pan Flute",
"Blown Bottle",
"Shakuhachi",
"Whistle",
"Ocarina",
"Lead 1 (square)",
"Lead 2 (sawtooth)",
"Lead 3 (calliope)",
"Lead 4 (chiff)",
"Lead 5 (charang)",
"Lead 6 (voice)",
"Lead 7 (fifths)",
"Lead 8 (bass + lead)",
"Pad 1 (new age)",
"Pad 2 (warm)",
"Pad 3 (polysynth)",
"Pad 4 (choir)",
"Pad 5 (bowed)",
"Pad 6 (metallic)",
"Pad 7 (halo)",
"Pad 8 (sweep)",
"FX 1 (rain)",
"FX 2 (soundtrack)",
"FX 3 (crystal)",
"FX 4 (atmosphere)",
"FX 5 (brightness)",
"FX 6 (goblins)",
"FX 7 (echoes)",
"FX 8 (sci-fi)",
"Sitar",
"Banjo",
"Shamisen",
"Koto",
"Kalimba",
"Bagpipe",
"Fiddle",
"Shanai",
"Tinkle Bell",
"Agogo",
"Steel Drums",
"Woodblock",
"Taiko Drum",
"Melodic Tom",
"Synth Drum",
"Reverse Cymbal",
"Guitar Fret Noise",
"Breath Noise",
"Seashore",
"Bird Tweet",
"Telephone Ring",
"Helicopter",
"Applause",
"Gunshot"
]
# The encoders and tools are expected in your PATH. You can supply alternate
# paths by changing the constants below.
LAME = "lame" # `which lame`.chomp
FLUIDSYNTH = "fluidsynth" # `which fluidsynth`.chomp
puts "Building the following instruments using font: " + SOUNDFONT
# Display instrument names.
INSTRUMENTS.each do |i|
puts " #{i}: " + MIDIJS_PATCH_NAMES[i]
end
puts
puts "Using MP3 encoder: " + LAME
puts "Using FluidSynth encoder: " + FLUIDSYNTH
puts
puts "Sending output to: " + BUILD_DIR
puts
raise "Can't find soundfont: #{SOUNDFONT}" unless File.exist? SOUNDFONT
raise "Can't find 'lame' command" if LAME.empty?
raise "Can't find 'fluidsynth' command" if FLUIDSYNTH.empty?
raise "Output directory does not exist: #{BUILD_DIR}" unless File.exist?(BUILD_DIR)
puts "Hit return to begin."
$stdin.readline
NOTES = {
"C" => 0,
"Db" => 1,
"D" => 2,
"Eb" => 3,
"E" => 4,
"F" => 5,
"Gb" => 6,
"G" => 7,
"Ab" => 8,
"A" => 9,
"Bb" => 10,
"B" => 11
}
MIDI_C0 = 12
VELOCITY = 100
DURATION = Integer(3000)
TEMP_FILE = "#{BUILD_DIR}/%s%stemp.midi"
FLUIDSYNTH_RAW = "%s.wav"
def deflate(string, level)
z = Zlib::Deflate.new(level)
dst = z.deflate(string, Zlib::FINISH)
z.close
dst
end
def note_to_int(note, octave)
value = NOTES[note]
increment = MIDI_C0 * octave
return value + increment
end
def int_to_note(value)
raise "Bad Value" if value < MIDI_C0
reverse_notes = NOTES.invert
value -= MIDI_C0
octave = value / 12
note = value % 12
return { key: reverse_notes[note],
octave: octave }
end
# Run a quick table validation
MIDI_C0.upto(100) do |x|
note = int_to_note x
#raise "Broken table" unless note_to_int(note[:key], note[:octave]) == x
end
def generate_midi(program, note_value, file)
include MIDI
seq = Sequence.new()
track = Track.new(seq)
seq.tracks << track
track.events << ProgramChange.new(0, Integer(program))
track.events << NoteOn.new(0, note_value, VELOCITY, 0) # channel, note, velocity, delta
track.events << NoteOff.new(0, note_value, VELOCITY, DURATION)
File.open(file, 'wb') { | file | seq.write(file) }
end
def run_command(cmd)
puts "Running: " + cmd
`#{cmd}`
end
def midi_to_audio(source, target)
run_command "#{FLUIDSYNTH} -C no -R no -g 0.5 -F #{target} #{SOUNDFONT} #{source}"
run_command "#{LAME} -v -b 8 -B 64 #{target}"
rm target
end
def open_js_file(instrument_key, type)
js_file = File.open("#{BUILD_DIR}/#{instrument_key}-#{type}.js", "w")
js_file.write(
"""
if (typeof(MIDI) === 'undefined') var MIDI = {};
if (typeof(MIDI.Soundfont) === 'undefined') MIDI.Soundfont = {};
MIDI.Soundfont.#{instrument_key} = {
""")
return js_file
end
def close_js_file(file)
file.write("\n}\n")
file.close
end
def base64js(note, file, type)
output = '"' + note + '": '
output += '"' + "data:audio/#{type};base64,"
output += Base64.strict_encode64(File.read(file)) + '"'
return output
end
def generate_audio(program)
instrument = MIDIJS_PATCH_NAMES[program]
instrument_key = instrument.downcase.gsub(/[^a-z0-9 ]/, "").gsub(/[ ]/, "_")
puts "Generating audio for: " + instrument + "(#{instrument_key})"
mkdir_p "#{BUILD_DIR}/#{instrument_key}"
note_to_int("A", 0).upto(note_to_int("C", 8)) do |note_value|
output_name = "p#{note_value}_v#{VELOCITY}"
output_path_prefix = BUILD_DIR + "/#{instrument_key}" + output_name
puts "Generating: #{output_name}"
temp_file_specific = TEMP_FILE % [output_name, instrument_key]
generate_midi(program, note_value, temp_file_specific)
midi_to_audio(temp_file_specific, output_path_prefix + ".wav")
mv output_path_prefix + ".mp3", "#{BUILD_DIR}/#{instrument_key}/#{output_name}.mp3"
rm temp_file_specific
end
tempHash = {
"name" => instrument_key,
"minPitch" => 0,
"maxPitch" => 127,
"durationSeconds" => 3.0,
"releaseSeconds" => 1.0,
"percussive": false,
"velocities": [100]
}
File.open("#{BUILD_DIR}/#{instrument_key}/instrument.json", "w") do |f|
f.write(tempHash.to_json)
end
end
Parallel.each(INSTRUMENTS, :in_processes=>Etc.nprocessors){|i| generate_audio(i)}

View File

@ -122,6 +122,10 @@ func (a *App) CopyFile(src string, dst string) error {
}
func (a *App) OpenSaveFileDialog(filterPattern string, defaultFileName string, savedContent string) (string, error) {
return a.OpenSaveFileDialogBytes(filterPattern, defaultFileName, []byte(savedContent))
}
func (a *App) OpenSaveFileDialogBytes(filterPattern string, defaultFileName string, savedContent []byte) (string, error) {
path, err := wruntime.SaveFileDialog(a.ctx, wruntime.SaveDialogOptions{
DefaultFilename: defaultFileName,
Filters: []wruntime.FileFilter{{
@ -135,7 +139,7 @@ func (a *App) OpenSaveFileDialog(filterPattern string, defaultFileName string, s
if path == "" {
return "", nil
}
if err := os.WriteFile(path, []byte(savedContent), 0644); err != nil {
if err := os.WriteFile(path, savedContent, 0644); err != nil {
return "", err
}
return path, nil

File diff suppressed because it is too large Load Diff

View File

@ -11,11 +11,13 @@
"dependencies": {
"@fluentui/react-components": "^9.20.0",
"@fluentui/react-icons": "^2.0.201",
"@magenta/music": "^1.23.1",
"@microsoft/fetch-event-source": "^2.0.1",
"@primer/octicons-react": "^19.1.0",
"chart.js": "^4.3.0",
"classnames": "^2.3.2",
"github-markdown-css": "^5.2.0",
"html-midi-player": "^1.5.0",
"i18next": "^22.4.15",
"mobx": "^6.9.0",
"mobx-react-lite": "^3.4.3",

View File

@ -234,5 +234,11 @@
"Failed to merge model": "合并模型失败",
"The data path should be a directory or a file in jsonl format (more formats will be supported in the future).\n\nWhen you provide a directory path, all the txt files within that directory will be automatically converted into training data. This is commonly used for large-scale training in writing, code generation, or knowledge bases.\n\nThe jsonl format file can be referenced at https://github.com/Abel2076/json2binidx_tool/blob/main/sample.jsonl.\nYou can also write it similar to OpenAI's playground format, as shown in https://platform.openai.com/playground/p/default-chat.\nEven for multi-turn conversations, they must be written in a single line using `\\n` to indicate line breaks. If they are different dialogues or topics, they should be written in separate lines.": "数据路径必须是一个文件夹或者jsonl格式文件 (未来会支持更多格式)\n\n当你填写的路径是一个文件夹时该文件夹内的所有txt文件会被自动转换为训练数据通常这用于大批量训练写作代码生成或知识库\n\njsonl文件的格式参考 https://github.com/Abel2076/json2binidx_tool/blob/main/sample.jsonl\n你也可以仿照openai的playground编写参考 https://platform.openai.com/playground/p/default-chat\n即使是多轮对话也必须写在一行用`\\n`表示换行,如果是不同对话或主题,则另起一行",
"Size mismatch for blocks. You are attempting to continue training from the LoRA model, but it does not match the base model. Please set LoRA model to None.": "尺寸不匹配块。你正在尝试从LoRA模型继续训练但该LoRA模型与基底模型不匹配请将LoRA模型设为空",
"Instruction: Write a story using the following information\n\nInput: A man named Alex chops a tree down\n\nResponse:": "Instruction: Write a story using the following information\n\nInput: 艾利克斯砍倒了一棵树\n\nResponse:"
"Instruction: Write a story using the following information\n\nInput: A man named Alex chops a tree down\n\nResponse:": "Instruction: Write a story using the following information\n\nInput: 艾利克斯砍倒了一棵树\n\nResponse:",
"Composition": "作曲",
"Use Local Sound Font": "使用本地音色资源",
"Auto Play At The End": "结束时自动播放",
"No File to save": "无文件可保存",
"File Saved": "文件已保存",
"Failed to load local sound font, please check if the files exist - assets/sound-font": "加载本地音色资源失败,请检查文件是否存在 - assets/sound-font"
}

View File

@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
import { ArrowReset20Regular } from '@fluentui/react-icons';
import commonStore from '../stores/commonStore';
import { defaultModelConfigs, defaultModelConfigsMac } from '../pages/defaultModelConfigs';
import { defaultModelConfigs, defaultModelConfigsMac } from '../pages/defaultConfigs';
export const ResetConfigsButton: FC<{ afterConfirm?: () => void }> = ({ afterConfirm }) => {
const { t } = useTranslation();

View File

@ -6,6 +6,7 @@ import App from './App';
import { HashRouter } from 'react-router-dom';
import { startup } from './startup';
import './_locales/i18n-react';
import 'html-midi-player';
import { WindowShow } from '../wailsjs/runtime';
startup().then(() => {

View File

@ -13,6 +13,7 @@ import { DialogButton } from '../components/DialogButton';
import { PresetsButton } from './PresetsManager/PresetsButton';
import { ToolTipButton } from '../components/ToolTipButton';
import { ArrowSync20Regular } from '@fluentui/react-icons';
import { defaultPresets } from './defaultConfigs';
export type CompletionParams = Omit<ApiParameters, 'apiPort'> & {
stop: string,
@ -26,112 +27,6 @@ export type CompletionPreset = {
params: CompletionParams
}
export const defaultPresets: CompletionPreset[] = [{
name: 'Writer',
prompt: 'The following is an epic science fiction masterpiece that is immortalized, with delicate descriptions and grand depictions of interstellar civilization wars.\nChapter 1.\n',
params: {
maxResponseToken: 500,
temperature: 1.2,
topP: 0.5,
presencePenalty: 0.4,
frequencyPenalty: 0.4,
stop: '\\n\\nUser',
injectStart: '',
injectEnd: ''
}
}, {
name: 'Translator',
prompt: 'Translate this into Chinese.\n\nEnglish: What rooms do you have available?',
params: {
maxResponseToken: 500,
temperature: 1,
topP: 0.3,
presencePenalty: 0,
frequencyPenalty: 1,
stop: '\\n\\n',
injectStart: '\\nChinese: ',
injectEnd: '\\n\\nEnglish: '
}
}, {
name: 'Catgirl',
prompt: 'The following is a conversation between a cat girl and her owner. The cat girl is a humanized creature that behaves like a cat but is humanoid. At the end of each sentence in the dialogue, she will add \"Meow~\". In the following content, User represents the owner and Assistant represents the cat girl.\n\nUser: Hello.\n\nAssistant: I\'m here, meow~.\n\nUser: Can you tell jokes?',
params: {
maxResponseToken: 500,
temperature: 1.2,
topP: 0.5,
presencePenalty: 0.4,
frequencyPenalty: 0.4,
stop: '\\n\\nUser',
injectStart: '\\n\\nAssistant: ',
injectEnd: '\\n\\nUser: '
}
}, {
name: 'Chinese Kongfu',
prompt: 'User: 请你扮演一个文本冒险游戏,我是游戏主角。这是一个玄幻修真世界,有四大门派。我输入我的行动,请你显示行动结果,并具体描述环境。我的第一个行动是“醒来”,请开始故事。',
params: {
maxResponseToken: 500,
temperature: 1.1,
topP: 0.7,
presencePenalty: 0.3,
frequencyPenalty: 0.3,
stop: '\\n\\nUser',
injectStart: '\\n\\nAssistant: ',
injectEnd: '\\n\\nUser: '
}
}, {
name: 'Code Generation',
prompt: 'def sum(',
params: {
maxResponseToken: 500,
temperature: 1,
topP: 0.3,
presencePenalty: 0,
frequencyPenalty: 1,
stop: '\\n\\n',
injectStart: '',
injectEnd: ''
}
}, {
name: 'Werewolf',
prompt: 'There is currently a game of Werewolf with six players, including a Seer (who can check identities at night), two Werewolves (who can choose someone to kill at night), a Bodyguard (who can choose someone to protect at night), two Villagers (with no special abilities), and a game host. User will play as Player 1, Assistant will play as Players 2-6 and the game host, and they will begin playing together. Every night, the host will ask User for his action and simulate the actions of the other players. During the day, the host will oversee the voting process and ask User for his vote. \n\nAssistant: Next, I will act as the game host and assign everyone their roles, including randomly assigning yours. Then, I will simulate the actions of Players 2-6 and let you know what happens each day. Based on your assigned role, you can tell me your actions and I will let you know the corresponding results each day.\n\nUser: Okay, I understand. Let\'s begin. Please assign me a role. Am I the Seer, Werewolf, Villager, or Bodyguard?\n\nAssistant: You are the Seer. Now that night has fallen, please choose a player to check his identity.\n\nUser: Tonight, I want to check Player 2 and find out his role.',
params: {
maxResponseToken: 500,
temperature: 1.2,
topP: 0.4,
presencePenalty: 0.5,
frequencyPenalty: 0.5,
stop: '\\n\\nUser',
injectStart: '\\n\\nAssistant: ',
injectEnd: '\\n\\nUser: '
}
}, {
name: 'Instruction',
prompt: 'Instruction: Write a story using the following information\n\nInput: A man named Alex chops a tree down\n\nResponse:',
params: {
maxResponseToken: 500,
temperature: 1,
topP: 0.3,
presencePenalty: 0,
frequencyPenalty: 1,
stop: '',
injectStart: '',
injectEnd: ''
}
}, {
name: 'Blank',
prompt: '',
params: {
maxResponseToken: 500,
temperature: 1,
topP: 0.3,
presencePenalty: 0,
frequencyPenalty: 1,
stop: '',
injectStart: '',
injectEnd: ''
}
}];
let completionSseController: AbortController | null = null;
const CompletionPanel: FC = observer(() => {

View File

@ -0,0 +1,345 @@
import React, { FC, useEffect, useRef } from 'react';
import { observer } from 'mobx-react-lite';
import { WorkHeader } from '../components/WorkHeader';
import { Button, Checkbox, Textarea } from '@fluentui/react-components';
import { Labeled } from '../components/Labeled';
import { ValuedSlider } from '../components/ValuedSlider';
import { useTranslation } from 'react-i18next';
import commonStore, { ModelStatus } from '../stores/commonStore';
import { fetchEventSource } from '@microsoft/fetch-event-source';
import { toast } from 'react-toastify';
import { DialogButton } from '../components/DialogButton';
import { ToolTipButton } from '../components/ToolTipButton';
import { ArrowSync20Regular, Save28Regular } from '@fluentui/react-icons';
import { PlayerElement, VisualizerElement } from 'html-midi-player';
import * as mm from '@magenta/music/esm/core.js';
import { NoteSequence } from '@magenta/music/esm/protobuf.js';
import { defaultCompositionPrompt } from './defaultConfigs';
import { FileExists, OpenFileFolder, OpenSaveFileDialogBytes } from '../../wailsjs/go/backend_golang/App';
import { toastWithButton } from '../utils';
export type CompositionParams = {
prompt: string,
maxResponseToken: number,
temperature: number,
topP: number,
autoPlay: boolean,
useLocalSoundFont: boolean,
midi: ArrayBuffer | null,
ns: NoteSequence | null
}
let compositionSseController: AbortController | null = null;
const CompositionPanel: FC = observer(() => {
const { t } = useTranslation();
const inputRef = useRef<HTMLTextAreaElement>(null);
const port = commonStore.getCurrentModelConfig().apiParameters.apiPort;
const visualizerRef = useRef<VisualizerElement>(null);
const playerRef = useRef<PlayerElement>(null);
const scrollToBottom = () => {
if (inputRef.current)
inputRef.current.scrollTop = inputRef.current.scrollHeight;
};
const params = commonStore.compositionParams;
const setParams = (newParams: Partial<CompositionParams>) => {
commonStore.setCompositionParams({
...commonStore.compositionParams,
...newParams
});
};
const setPrompt = (prompt: string) => {
setParams({
prompt
});
if (!commonStore.compositionGenerating)
generateNs(false);
};
const updateNs = (ns: NoteSequence | null) => {
if (playerRef.current) {
playerRef.current.noteSequence = ns;
playerRef.current.reload();
}
if (visualizerRef.current) {
visualizerRef.current.noteSequence = ns;
visualizerRef.current.reload();
}
};
const setSoundFont = async () => {
let soundUrl: string;
if (params.useLocalSoundFont)
soundUrl = 'assets/sound-font';
else
soundUrl = !commonStore.settings.giteeUpdatesSource ?
`https://raw.githubusercontent.com/josStorer/sgm_plus/master` :
`https://gitee.com/josc146/sgm_plus/raw/master`;
const fallbackUrl = 'https://cdn.jsdelivr.net/gh/josstorer/sgm_plus';
await fetch(soundUrl + '/soundfont.json').then(r => {
if (!r.ok)
soundUrl = fallbackUrl;
}).catch(() => soundUrl = fallbackUrl);
if (playerRef.current) {
playerRef.current.soundFont = soundUrl;
}
};
useEffect(() => {
if (inputRef.current)
inputRef.current.style.height = '100%';
scrollToBottom();
if (playerRef.current && visualizerRef.current) {
playerRef.current.addVisualizer(visualizerRef.current);
playerRef.current.addEventListener('start', () => {
visualizerRef.current?.reload();
});
setSoundFont().then(() => {
updateNs(params.ns);
});
const button = playerRef.current.shadowRoot?.querySelector('.controls .play') as HTMLElement | null;
if (button)
button.style.background = '#f2f5f6';
}
}, []);
const generateNs = (autoPlay: boolean) => {
fetch(commonStore.settings.apiUrl ?
commonStore.settings.apiUrl + '/text-to-midi' :
`http://127.0.0.1:${port}/text-to-midi`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
'text': params.prompt.replaceAll(/<pad>|<start>|<end>/g, '').replaceAll(' ', '').trim()
})
}).then(r => {
r.arrayBuffer().then(midi => {
const ns = mm.midiToSequenceProto(midi);
setParams({
midi,
ns
});
updateNs(ns);
if (autoPlay) {
playerRef.current?.start();
}
});
});
};
const onSubmit = (prompt: string) => {
commonStore.setCompositionSubmittedPrompt(prompt);
if (commonStore.status.status === ModelStatus.Offline && !commonStore.settings.apiUrl) {
toast(t('Please click the button in the top right corner to start the model'), { type: 'warning' });
commonStore.setCompositionGenerating(false);
return;
}
let answer = '';
compositionSseController = new AbortController();
fetchEventSource( // https://api.openai.com/v1/completions || http://127.0.0.1:${port}/completions
commonStore.settings.apiUrl ?
commonStore.settings.apiUrl + '/v1/completions' :
`http://127.0.0.1:${port}/completions`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${commonStore.settings.apiKey}`
},
body: JSON.stringify({
prompt,
stream: true,
model: commonStore.settings.apiCompletionModelName, // 'text-davinci-003'
max_tokens: params.maxResponseToken,
temperature: params.temperature,
top_p: params.topP
}),
signal: compositionSseController?.signal,
onmessage(e) {
scrollToBottom();
if (e.data.trim() === '[DONE]') {
commonStore.setCompositionGenerating(false);
generateNs(params.autoPlay);
return;
}
let data;
try {
data = JSON.parse(e.data);
} catch (error) {
console.debug('json error', error);
return;
}
if (data.choices && Array.isArray(data.choices) && data.choices.length > 0) {
answer += data.choices[0]?.text || data.choices[0]?.delta?.content || '';
setPrompt(prompt + answer.replace(/\s+$/, ''));
}
},
async onopen(response) {
if (response.status !== 200) {
toast(response.statusText + '\n' + (await response.text()), {
type: 'error'
});
}
},
onclose() {
console.log('Connection closed');
},
onerror(err) {
err = err.message || err;
if (err && !err.includes('ReadableStreamDefaultReader'))
toast(err, {
type: 'error'
});
commonStore.setCompositionGenerating(false);
throw err;
}
});
};
return (
<div className="flex flex-col gap-2 overflow-hidden grow">
<div className="flex flex-col sm:flex-row gap-2 overflow-hidden grow">
<Textarea
ref={inputRef}
className="grow"
value={params.prompt}
onChange={(e) => {
commonStore.setCompositionSubmittedPrompt(e.target.value);
setPrompt(e.target.value);
}}
/>
<div className="flex flex-col gap-1 max-h-48 sm:max-w-sm sm:max-h-full overflow-x-hidden overflow-y-auto p-1">
<Labeled flex breakline label={t('Max Response Token')}
desc={t('By default, the maximum number of tokens that can be answered in a single response, it can be changed by the user by specifying API parameters.')}
content={
<ValuedSlider value={params.maxResponseToken} min={100} max={4100}
step={100}
input
onChange={(e, data) => {
setParams({
maxResponseToken: data.value
});
}} />
} />
<Labeled flex breakline label={t('Temperature')}
desc={t('Sampling temperature, it\'s like giving alcohol to a model, the higher the stronger the randomness and creativity, while the lower, the more focused and deterministic it will be.')}
content={
<ValuedSlider value={params.temperature} min={0} max={2} step={0.1}
input
onChange={(e, data) => {
setParams({
temperature: data.value
});
}} />
} />
<Labeled flex breakline label={t('Top_P')}
desc={t('Just like feeding sedatives to the model. Consider the results of the top n% probability mass, 0.1 considers the top 10%, with higher quality but more conservative, 1 considers all results, with lower quality but more diverse.')}
content={
<ValuedSlider value={params.topP} min={0} max={1} step={0.1} input
onChange={(e, data) => {
setParams({
topP: data.value
});
}} />
} />
<div className="grow" />
<Checkbox className="select-none"
size="large" label={t('Use Local Sound Font')} checked={params.useLocalSoundFont}
onChange={async (_, data) => {
if (data.checked) {
if (!await FileExists('assets/sound-font/accordion/instrument.json')) {
toast(t('Failed to load local sound font, please check if the files exist - assets/sound-font'),
{ type: 'warning' });
return;
}
}
setParams({
useLocalSoundFont: data.checked as boolean
});
setSoundFont();
}} />
<Checkbox className="select-none"
size="large" label={t('Auto Play At The End')} checked={params.autoPlay} onChange={(_, data) => {
setParams({
autoPlay: data.checked as boolean
});
}} />
<div className="flex justify-between gap-2">
<ToolTipButton desc={t('Regenerate')} icon={<ArrowSync20Regular />} onClick={() => {
compositionSseController?.abort();
commonStore.setCompositionGenerating(true);
setPrompt(commonStore.compositionSubmittedPrompt);
onSubmit(commonStore.compositionSubmittedPrompt);
}} />
<DialogButton className="grow" text={t('Reset')} title={t('Reset')}
contentText={t('Are you sure you want to reset this page? It cannot be undone.')}
onConfirm={() => {
commonStore.setCompositionSubmittedPrompt(defaultCompositionPrompt);
setPrompt(defaultCompositionPrompt);
}} />
<Button className="grow" appearance="primary" onClick={() => {
if (commonStore.compositionGenerating) {
compositionSseController?.abort();
commonStore.setCompositionGenerating(false);
generateNs(params.autoPlay);
} else {
commonStore.setCompositionGenerating(true);
onSubmit(params.prompt);
}
}}>{!commonStore.compositionGenerating ? t('Generate') : t('Stop')}</Button>
</div>
</div>
</div>
<div className="flex flex-col">
<div className="ml-auto mr-auto">
<midi-visualizer
ref={visualizerRef}
type="waterfall"
/>
</div>
<div className="flex">
<midi-player
ref={playerRef}
style={{ width: '100%' }}
/>
<Button icon={<Save28Regular />}
onClick={() => {
if (params.midi) {
OpenSaveFileDialogBytes('*.mid', 'music.mid', Array.from(new Uint8Array(params.midi))).then((path) => {
if (path)
toastWithButton(t('File Saved'), t('Open'), () => {
OpenFileFolder(path, false);
});
}).catch((e: any) => {
toast(t('Error') + ' - ' + (e.message || e), { type: 'error', autoClose: 2500 });
});
} else {
toast(t('No File to save'), { type: 'warning', autoClose: 1500 });
}
}}
>
{t('Save')}
</Button>
</div>
</div>
</div>
);
});
export const Composition: FC = observer(() => {
return (
<div className="flex flex-col gap-1 p-2 h-full overflow-hidden">
<WorkHeader />
<CompositionPanel />
</div>
);
});

View File

@ -1,4 +1,113 @@
import { ModelConfig } from './Configs';
import { CompletionPreset } from './Completion';
export const defaultCompositionPrompt = '<pad>';
export const defaultPresets: CompletionPreset[] = [{
name: 'Writer',
prompt: 'The following is an epic science fiction masterpiece that is immortalized, with delicate descriptions and grand depictions of interstellar civilization wars.\nChapter 1.\n',
params: {
maxResponseToken: 500,
temperature: 1.2,
topP: 0.5,
presencePenalty: 0.4,
frequencyPenalty: 0.4,
stop: '\\n\\nUser',
injectStart: '',
injectEnd: ''
}
}, {
name: 'Translator',
prompt: 'Translate this into Chinese.\n\nEnglish: What rooms do you have available?',
params: {
maxResponseToken: 500,
temperature: 1,
topP: 0.3,
presencePenalty: 0,
frequencyPenalty: 1,
stop: '\\n\\n',
injectStart: '\\nChinese: ',
injectEnd: '\\n\\nEnglish: '
}
}, {
name: 'Catgirl',
prompt: 'The following is a conversation between a cat girl and her owner. The cat girl is a humanized creature that behaves like a cat but is humanoid. At the end of each sentence in the dialogue, she will add \"Meow~\". In the following content, User represents the owner and Assistant represents the cat girl.\n\nUser: Hello.\n\nAssistant: I\'m here, meow~.\n\nUser: Can you tell jokes?',
params: {
maxResponseToken: 500,
temperature: 1.2,
topP: 0.5,
presencePenalty: 0.4,
frequencyPenalty: 0.4,
stop: '\\n\\nUser',
injectStart: '\\n\\nAssistant: ',
injectEnd: '\\n\\nUser: '
}
}, {
name: 'Chinese Kongfu',
prompt: 'User: 请你扮演一个文本冒险游戏,我是游戏主角。这是一个玄幻修真世界,有四大门派。我输入我的行动,请你显示行动结果,并具体描述环境。我的第一个行动是“醒来”,请开始故事。',
params: {
maxResponseToken: 500,
temperature: 1.1,
topP: 0.7,
presencePenalty: 0.3,
frequencyPenalty: 0.3,
stop: '\\n\\nUser',
injectStart: '\\n\\nAssistant: ',
injectEnd: '\\n\\nUser: '
}
}, {
name: 'Code Generation',
prompt: 'def sum(',
params: {
maxResponseToken: 500,
temperature: 1,
topP: 0.3,
presencePenalty: 0,
frequencyPenalty: 1,
stop: '\\n\\n',
injectStart: '',
injectEnd: ''
}
}, {
name: 'Werewolf',
prompt: 'There is currently a game of Werewolf with six players, including a Seer (who can check identities at night), two Werewolves (who can choose someone to kill at night), a Bodyguard (who can choose someone to protect at night), two Villagers (with no special abilities), and a game host. User will play as Player 1, Assistant will play as Players 2-6 and the game host, and they will begin playing together. Every night, the host will ask User for his action and simulate the actions of the other players. During the day, the host will oversee the voting process and ask User for his vote. \n\nAssistant: Next, I will act as the game host and assign everyone their roles, including randomly assigning yours. Then, I will simulate the actions of Players 2-6 and let you know what happens each day. Based on your assigned role, you can tell me your actions and I will let you know the corresponding results each day.\n\nUser: Okay, I understand. Let\'s begin. Please assign me a role. Am I the Seer, Werewolf, Villager, or Bodyguard?\n\nAssistant: You are the Seer. Now that night has fallen, please choose a player to check his identity.\n\nUser: Tonight, I want to check Player 2 and find out his role.',
params: {
maxResponseToken: 500,
temperature: 1.2,
topP: 0.4,
presencePenalty: 0.5,
frequencyPenalty: 0.5,
stop: '\\n\\nUser',
injectStart: '\\n\\nAssistant: ',
injectEnd: '\\n\\nUser: '
}
}, {
name: 'Instruction',
prompt: 'Instruction: Write a story using the following information\n\nInput: A man named Alex chops a tree down\n\nResponse:',
params: {
maxResponseToken: 500,
temperature: 1,
topP: 0.3,
presencePenalty: 0,
frequencyPenalty: 1,
stop: '',
injectStart: '',
injectEnd: ''
}
}, {
name: 'Blank',
prompt: '',
params: {
maxResponseToken: 500,
temperature: 1,
topP: 0.3,
presencePenalty: 0,
frequencyPenalty: 1,
stop: '',
injectStart: '',
injectEnd: ''
}
}];
export const defaultModelConfigsMac: ModelConfig[] = [
{

View File

@ -8,6 +8,7 @@ import {
DocumentSettings20Regular,
Home20Regular,
Info20Regular,
MusicNote220Regular,
Settings20Regular,
Storage20Regular
} from '@fluentui/react-icons';
@ -19,6 +20,7 @@ import { Settings } from './Settings';
import { About } from './About';
import { Downloads } from './Downloads';
import { Completion } from './Completion';
import { Composition } from './Composition';
type NavigationItem = {
label: string;
@ -50,6 +52,13 @@ export const pages: NavigationItem[] = [
element: <Completion />,
top: true
},
{
label: 'Composition',
path: '/composition',
icon: <MusicNote220Regular />,
element: <Composition />,
top: true
},
{
label: 'Configs',
path: '/configs',

View File

@ -4,7 +4,7 @@ import { Cache, checkUpdate, downloadProgramFiles, LocalConfig, refreshLocalMode
import { getStatus } from './apis';
import { EventsOn } from '../wailsjs/runtime';
import manifest from '../../manifest.json';
import { defaultModelConfigs, defaultModelConfigsMac } from './pages/defaultModelConfigs';
import { defaultModelConfigs, defaultModelConfigsMac } from './pages/defaultConfigs';
import { Preset } from './pages/PresetsManager/PresetsButton';
import { wslHandler } from './pages/Train';

View File

@ -11,11 +11,12 @@ import { IntroductionContent } from '../pages/Home';
import { AboutContent } from '../pages/About';
import i18n from 'i18next';
import { CompletionPreset } from '../pages/Completion';
import { defaultModelConfigs, defaultModelConfigsMac } from '../pages/defaultModelConfigs';
import { defaultCompositionPrompt, defaultModelConfigs, defaultModelConfigsMac } from '../pages/defaultConfigs';
import commonStore from './commonStore';
import { Preset } from '../pages/PresetsManager/PresetsButton';
import { DataProcessParameters, LoraFinetuneParameters } from '../pages/Train';
import { ChartData } from 'chart.js';
import { CompositionParams } from '../pages/Composition';
export enum ModelStatus {
Offline,
@ -57,6 +58,19 @@ class CommonStore {
completionPreset: CompletionPreset | null = null;
completionGenerating: boolean = false;
completionSubmittedPrompt: string = '';
// composition
compositionParams: CompositionParams = {
prompt: defaultCompositionPrompt,
maxResponseToken: 200,
temperature: 1,
topP: 0.8,
autoPlay: true,
useLocalSoundFont: false,
midi: null,
ns: null
};
compositionGenerating: boolean = false;
compositionSubmittedPrompt: string = defaultCompositionPrompt;
// configs
currentModelConfigIndex: number = 0;
modelConfigs: ModelConfig[] = [];
@ -267,6 +281,18 @@ class CommonStore {
this.completionSubmittedPrompt = value;
}
setCompositionParams(value: CompositionParams) {
this.compositionParams = value;
}
setCompositionGenerating(value: boolean) {
this.compositionGenerating = value;
}
setCompositionSubmittedPrompt(value: string) {
this.compositionSubmittedPrompt = value;
}
setWslStdout(value: string) {
this.wslStdout = value;
}

View File

@ -28,6 +28,7 @@ body {
/* Works on Chrome, Edge, and Safari */
*::-webkit-scrollbar {
width: 9px;
height: 9px;
}
*::-webkit-scrollbar-thumb {
@ -92,3 +93,22 @@ body {
}
}
}
midi-player {
&::part(control-panel) {
background: none;
}
}
midi-visualizer {
$instrument-colors: #007bff, #20c997, #dc3545, #6610f2, #ffc107, #e83e8c, #17a2b8, #fd7e14, #28a745;
svg {
@for $i from 0 to 200 {
$color: nth($instrument-colors, ($i % length($instrument-colors)) + 1);
rect.note[data-instrument="#{$i}"] {
fill: $color;
}
}
}
}

View File

@ -0,0 +1,9 @@
declare module JSX {
import { PlayerElement } from 'html-midi-player';
import { VisualizerElement } from 'html-midi-player';
interface IntrinsicElements {
'midi-player': PlayerElement;
'midi-visualizer': VisualizerElement;
}
}

View File

@ -34,6 +34,8 @@ export function OpenFileFolder(arg1:string,arg2:boolean):Promise<void>;
export function OpenSaveFileDialog(arg1:string,arg2:string,arg3:string):Promise<string>;
export function OpenSaveFileDialogBytes(arg1:string,arg2:string,arg3:Array<number>):Promise<string>;
export function PauseDownload(arg1:string):Promise<void>;
export function ReadFileInfo(arg1:string):Promise<backend_golang.FileInfo>;

View File

@ -66,6 +66,10 @@ export function OpenSaveFileDialog(arg1, arg2, arg3) {
return window['go']['backend_golang']['App']['OpenSaveFileDialog'](arg1, arg2, arg3);
}
export function OpenSaveFileDialogBytes(arg1, arg2, arg3) {
return window['go']['backend_golang']['App']['OpenSaveFileDialogBytes'](arg1, arg2, arg3);
}
export function PauseDownload(arg1) {
return window['go']['backend_golang']['App']['PauseDownload'](arg1);
}

35
main.go
View File

@ -2,6 +2,9 @@ package main
import (
"embed"
"fmt"
"net/http"
"os"
"runtime/debug"
"strings"
@ -13,6 +16,27 @@ import (
"github.com/wailsapp/wails/v2/pkg/options/windows"
)
type FileLoader struct {
http.Handler
}
func NewFileLoader() *FileLoader {
return &FileLoader{}
}
func (h *FileLoader) ServeHTTP(res http.ResponseWriter, req *http.Request) {
var err error
requestedFilename := strings.TrimPrefix(req.URL.Path, "/")
println("Requesting file:", requestedFilename)
fileData, err := os.ReadFile(requestedFilename)
if err != nil {
res.WriteHeader(http.StatusBadRequest)
res.Write([]byte(fmt.Sprintf("Could not load file %s", requestedFilename)))
}
res.Write(fileData)
}
//go:embed all:frontend/dist
var assets embed.FS
@ -28,12 +52,20 @@ var py embed.FS
//go:embed finetune
var finetune embed.FS
//go:embed midi
var midi embed.FS
//go:embed assets/sound-font
var midiAssets embed.FS
func main() {
if buildInfo, ok := debug.ReadBuildInfo(); !ok || strings.Contains(buildInfo.String(), "-ldflags") {
backend.CopyEmbed(cyac)
backend.CopyEmbed(cyacInfo)
backend.CopyEmbed(py)
backend.CopyEmbed(finetune)
backend.CopyEmbed(midi)
backend.CopyEmbed(midiAssets)
}
// Create an instance of the app structure
@ -63,7 +95,8 @@ func main() {
IsZoomControlEnabled: true,
},
AssetServer: &assetserver.Options{
Assets: assets,
Assets: assets,
Handler: NewFileLoader(),
},
OnStartup: app.OnStartup,
Bind: []any{