2023-11-07 11:27:21 +00:00
import 'html-midi-player' ;
2023-07-28 04:30:05 +00:00
import React , { FC , useEffect , useRef } from 'react' ;
import { observer } from 'mobx-react-lite' ;
import { WorkHeader } from '../components/WorkHeader' ;
2023-11-28 07:34:06 +00:00
import { Button , Checkbox , Dropdown , Option , Textarea } from '@fluentui/react-components' ;
2023-07-28 04:30:05 +00:00
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' ;
2024-01-05 04:45:41 +00:00
import { defaultCompositionABCPrompt , defaultCompositionPrompt } from './defaultConfigs' ;
2023-11-29 06:05:58 +00:00
import {
CloseMidiPort ,
FileExists ,
OpenFileFolder ,
OpenMidiPort ,
2024-01-05 05:47:00 +00:00
OpenSaveFileDialog ,
2023-12-12 14:13:09 +00:00
OpenSaveFileDialogBytes ,
SaveFile ,
StartFile
2023-11-29 06:05:58 +00:00
} from '../../wailsjs/go/backend_golang/App' ;
2023-11-29 11:04:41 +00:00
import { getServerRoot , getSoundFont , toastWithButton } from '../utils' ;
2023-11-07 11:27:21 +00:00
import { CompositionParams } from '../types/composition' ;
2023-11-09 04:21:01 +00:00
import { useMediaQuery } from 'usehooks-ts' ;
2023-11-28 07:34:06 +00:00
import { AudiotrackButton } from './AudiotrackManager/AudiotrackButton' ;
2023-07-28 04:30:05 +00:00
let compositionSseController : AbortController | null = null ;
const CompositionPanel : FC = observer ( ( ) = > {
const { t } = useTranslation ( ) ;
2023-11-09 04:21:01 +00:00
const mq = useMediaQuery ( '(min-width: 640px)' ) ;
2023-07-28 04:30:05 +00:00
const inputRef = useRef < HTMLTextAreaElement > ( null ) ;
2024-01-05 05:47:00 +00:00
const modelConfig = commonStore . getCurrentModelConfig ( ) ;
const port = modelConfig . apiParameters . apiPort ;
const isABC = modelConfig . modelParameters . modelName . toLowerCase ( ) . includes ( 'abc' ) ;
2023-07-28 04:30:05 +00:00
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 ( ) = > {
if ( playerRef . current ) {
2023-11-29 11:04:41 +00:00
playerRef . current . soundFont = await getSoundFont ( ) ;
2023-07-28 04:30:05 +00:00
}
} ;
useEffect ( ( ) = > {
2023-11-20 12:16:55 +00:00
if ( inputRef . current ) {
2023-07-28 04:30:05 +00:00
inputRef . current . style . height = '100%' ;
2023-11-20 12:16:55 +00:00
inputRef . current . style . maxHeight = '100%' ;
}
2023-07-28 04:30:05 +00:00
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' ;
}
} , [ ] ) ;
2023-12-12 14:13:09 +00:00
const externalPlayListener = ( ) = > {
const params = commonStore . compositionParams ;
const saveAndPlay = async ( midi : ArrayBuffer , path : string ) = > {
await SaveFile ( path , Array . from ( new Uint8Array ( midi ) ) ) ;
StartFile ( path ) ;
} ;
if ( params . externalPlay ) {
if ( params . midi ) {
setTimeout ( ( ) = > {
playerRef . current ? . stop ( ) ;
} ) ;
saveAndPlay ( params . midi , './midi/last.mid' ) . catch ( ( e : string ) = > {
if ( e . includes ( 'being used' ) )
saveAndPlay ( params . midi ! , './midi/last-2.mid' ) ;
} ) ;
}
}
} ;
useEffect ( ( ) = > {
playerRef . current ? . addEventListener ( 'start' , externalPlayListener ) ;
return ( ) = > {
playerRef . current ? . removeEventListener ( 'start' , externalPlayListener ) ;
} ;
} , [ params . externalPlay ] ) ;
2023-11-30 03:57:52 +00:00
useEffect ( ( ) = > {
if ( ! ( commonStore . activeMidiDeviceIndex in commonStore . midiPorts ) ) {
commonStore . setActiveMidiDeviceIndex ( - 1 ) ;
CloseMidiPort ( ) ;
}
} , [ commonStore . midiPorts ] ) ;
2023-07-28 04:30:05 +00:00
const generateNs = ( autoPlay : boolean ) = > {
2024-01-05 05:47:00 +00:00
if ( commonStore . getCurrentModelConfig ( ) . modelParameters . modelName . toLowerCase ( ) . includes ( 'abc' ) ) {
import ( 'abcjs' ) . then ( ABCJS = > {
ABCJS . renderAbc ( 'abc-paper' , commonStore . compositionParams . prompt , { responsive : 'resize' } ) ;
} ) ;
return ;
}
2023-11-07 14:21:41 +00:00
fetch ( getServerRoot ( port ) + '/text-to-midi' , {
2023-07-28 04:30:05 +00:00
method : 'POST' ,
headers : {
'Content-Type' : 'application/json'
} ,
body : JSON.stringify ( {
2023-07-28 14:13:19 +00:00
'text' : commonStore . compositionParams . prompt . replaceAll ( /<pad>|<start>|<end>/g , '' ) . replaceAll ( ' ' , ' ' ) . trim ( )
2023-07-28 04:30:05 +00:00
} )
} ) . then ( r = > {
r . arrayBuffer ( ) . then ( midi = > {
const ns = mm . midiToSequenceProto ( midi ) ;
setParams ( {
midi ,
ns
} ) ;
updateNs ( ns ) ;
if ( autoPlay ) {
2023-12-12 14:13:09 +00:00
if ( commonStore . compositionParams . externalPlay )
externalPlayListener ( ) ;
2023-12-14 14:06:39 +00:00
else {
if ( commonStore . compositionParams . playOnlyGeneratedContent && playerRef . current ) {
playerRef . current . currentTime = Math . max ( commonStore . compositionParams . generationStartTime - 1 , 0 ) ;
}
2023-12-12 14:13:09 +00:00
setTimeout ( ( ) = > {
playerRef . current ? . start ( ) ;
} ) ;
2023-12-14 14:06:39 +00:00
}
2023-07-28 04:30:05 +00:00
}
} ) ;
} ) ;
} ;
const onSubmit = ( prompt : string ) = > {
commonStore . setCompositionSubmittedPrompt ( prompt ) ;
2023-11-07 11:27:21 +00:00
if ( commonStore . status . status === ModelStatus . Offline && ! commonStore . settings . apiUrl && commonStore . platform !== 'web' ) {
2023-07-28 04:30:05 +00:00
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 ( ) ;
2023-11-07 14:21:41 +00:00
fetchEventSource ( // https://api.openai.com/v1/completions || http://127.0.0.1:${port}/v1/completions
2023-12-12 14:37:36 +00:00
getServerRoot ( port , true ) + '/v1/completions' ,
2023-07-28 04:30:05 +00:00
{
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 ) ;
2023-07-28 14:13:19 +00:00
generateNs ( commonStore . compositionParams . autoPlay ) ;
2023-07-28 04:30:05 +00:00
return ;
}
let data ;
try {
data = JSON . parse ( e . data ) ;
} catch ( error ) {
console . debug ( 'json error' , error ) ;
return ;
}
2023-11-20 15:27:44 +00:00
if ( data . model )
commonStore . setLastModelName ( data . model ) ;
2023-07-28 04:30:05 +00:00
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 ) ;
} }
/ >
2023-11-09 04:21:01 +00:00
< div className = "flex flex-col gap-1 max-h-48 sm:max-w-sm sm:max-h-full" >
< div className = "flex flex-col gap-1 grow 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 = {
2024-03-02 08:52:53 +00:00
< ValuedSlider value = { params . temperature } min = { 0 } max = { 3 } step = { 0.1 }
2023-11-09 04:21:01 +00:00
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 = {
2024-03-02 08:52:53 +00:00
< ValuedSlider value = { params . topP } min = { 0 } max = { 1 } step = { 0.05 } input
2023-11-09 04:21:01 +00:00
onChange = { ( e , data ) = > {
setParams ( {
topP : data.value
} ) ;
} } / >
} / >
< div className = "grow" / >
2023-12-10 15:08:44 +00:00
{
commonStore . platform !== 'web' &&
< 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 ;
}
2023-11-09 04:21:01 +00:00
}
2023-12-10 15:08:44 +00:00
setParams ( {
useLocalSoundFont : data.checked as boolean
} ) ;
setSoundFont ( ) ;
} } / >
}
2023-12-12 14:13:09 +00:00
{
commonStore . platform === 'windows' &&
< Checkbox className = "select-none"
size = "large" label = { t ( 'Play With External Player' ) } checked = { params . externalPlay }
onChange = { async ( _ , data ) = > {
setParams ( {
externalPlay : data.checked as boolean
} ) ;
} } / >
}
2023-11-09 04:21:01 +00:00
< Checkbox className = "select-none"
size = "large" label = { t ( 'Auto Play At The End' ) } checked = { params . autoPlay } onChange = { ( _ , data ) = > {
2023-07-28 04:30:05 +00:00
setParams ( {
2023-11-09 04:21:01 +00:00
autoPlay : data.checked as boolean
2023-07-28 04:30:05 +00:00
} ) ;
} } / >
2023-12-14 14:06:39 +00:00
< Checkbox className = "select-none"
size = "large" label = { t ( 'Only Auto Play Generated Content' ) } checked = { params . playOnlyGeneratedContent }
onChange = { async ( _ , data ) = > {
setParams ( {
autoPlay : data.checked as boolean || commonStore . compositionParams . autoPlay ,
playOnlyGeneratedContent : data.checked as boolean
} ) ;
} } / >
2023-12-10 15:08:44 +00:00
< Labeled flex breakline label = { t ( 'MIDI Input' ) }
desc = { t ( 'Select the MIDI input device to be used.' ) }
content = {
< div className = "flex flex-col gap-1" >
{
commonStore . platform !== 'web' &&
2023-11-29 06:05:58 +00:00
< Dropdown style = { { minWidth : 0 } }
2023-11-30 03:57:52 +00:00
value = { ( commonStore . activeMidiDeviceIndex === - 1 || ! ( commonStore . activeMidiDeviceIndex in commonStore . midiPorts ) )
? t ( 'None' ) !
: commonStore . midiPorts [ commonStore . activeMidiDeviceIndex ] . name }
2023-11-29 06:05:58 +00:00
selectedOptions = { [ commonStore . activeMidiDeviceIndex . toString ( ) ] }
onOptionSelect = { ( _ , data ) = > {
if ( data . optionValue ) {
const index = Number ( data . optionValue ) ;
let action = ( index === - 1 )
? ( ) = > CloseMidiPort ( )
: ( ) = > OpenMidiPort ( index ) ;
action ( ) . then ( ( ) = > {
commonStore . setActiveMidiDeviceIndex ( index ) ;
} ) . catch ( ( e ) = > {
toast ( t ( 'Error' ) + ' - ' + ( e . message || e ) , { type : 'error' , autoClose : 2500 } ) ;
} ) ;
}
} } >
< Option value = { '-1' } > { t ( 'None' ) ! } < / Option >
{ commonStore . midiPorts . map ( ( p , i ) = >
< Option key = { i } value = { i . toString ( ) } > { p . name } < / Option > )
}
2023-11-28 07:34:06 +00:00
< / Dropdown >
2023-12-10 15:08:44 +00:00
}
< AudiotrackButton setPrompt = { setPrompt } / >
< / div >
} / >
2023-11-09 04:21:01 +00:00
< / div >
2023-07-28 04:30:05 +00:00
< 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 = { ( ) = > {
2024-01-05 04:45:41 +00:00
const isABC = commonStore . getCurrentModelConfig ( ) . modelParameters . modelName . toLowerCase ( ) . includes ( 'abc' ) ;
const defaultPrompt = isABC ? defaultCompositionABCPrompt : defaultCompositionPrompt ;
commonStore . setCompositionSubmittedPrompt ( defaultPrompt ) ;
2023-12-14 14:06:39 +00:00
setParams ( {
generationStartTime : 0
} ) ;
2024-01-05 04:45:41 +00:00
setPrompt ( defaultPrompt ) ;
2023-07-28 04:30:05 +00:00
} } / >
< Button className = "grow" appearance = "primary" onClick = { ( ) = > {
if ( commonStore . compositionGenerating ) {
compositionSseController ? . abort ( ) ;
commonStore . setCompositionGenerating ( false ) ;
generateNs ( params . autoPlay ) ;
} else {
commonStore . setCompositionGenerating ( true ) ;
2023-12-14 14:06:39 +00:00
setParams ( {
generationStartTime : playerRef.current ? playerRef.current.duration : 0
} ) ;
2023-07-28 04:30:05 +00:00
onSubmit ( params . prompt ) ;
}
} } > { ! commonStore . compositionGenerating ? t ( 'Generate' ) : t ( 'Stop' ) } < / Button >
< / div >
< / div >
< / div >
< div className = "flex flex-col" >
< div className = "ml-auto mr-auto" >
2024-01-05 05:47:00 +00:00
{ isABC ? < div / > :
< midi - visualizer
ref = { visualizerRef }
type = "waterfall"
/ > }
2023-07-28 04:30:05 +00:00
< / div >
< div className = "flex" >
2024-01-05 05:47:00 +00:00
{ isABC ? < div className = "flex flex-col overflow-y-auto grow m-1" style = { { maxHeight : '260px' } } >
< div id = "abc-paper" / >
< / div > :
< midi - player
ref = { playerRef }
style = { { width : '100%' } }
/ > }
2023-11-09 04:21:01 +00:00
< Button icon = { < Save28Regular / > } size = { mq ? 'large' : 'medium' } appearance = { mq ? 'secondary' : 'subtle' }
2023-07-28 04:30:05 +00:00
onClick = { ( ) = > {
2024-01-05 05:47:00 +00:00
if ( isABC ) {
OpenSaveFileDialog ( '*.txt' , 'abc-music.txt' , commonStore . compositionParams . prompt ) . then ( ( path ) = > {
if ( path )
toastWithButton ( t ( 'File Saved' ) , t ( 'Open' ) , ( ) = > {
2024-02-02 14:00:01 +00:00
OpenFileFolder ( path ) ;
2024-01-05 05:47:00 +00:00
} ) ;
} ) . catch ( ( e ) = > {
toast ( t ( 'Error' ) + ' - ' + ( e . message || e ) , { type : 'error' , autoClose : 2500 } ) ;
} ) ;
return ;
}
2023-07-28 04:30:05 +00:00
if ( params . midi ) {
OpenSaveFileDialogBytes ( '*.mid' , 'music.mid' , Array . from ( new Uint8Array ( params . midi ) ) ) . then ( ( path ) = > {
if ( path )
toastWithButton ( t ( 'File Saved' ) , t ( 'Open' ) , ( ) = > {
2024-02-02 14:00:01 +00:00
OpenFileFolder ( path ) ;
2023-07-28 04:30:05 +00:00
} ) ;
2023-10-24 15:41:18 +00:00
} ) . catch ( ( e ) = > {
2023-07-28 04:30:05 +00:00
toast ( t ( 'Error' ) + ' - ' + ( e . message || e ) , { type : 'error' , autoClose : 2500 } ) ;
} ) ;
} else {
toast ( t ( 'No File to save' ) , { type : 'warning' , autoClose : 1500 } ) ;
}
} }
>
2023-11-09 04:21:01 +00:00
{ mq ? t ( 'Save' ) : '' }
2023-07-28 04:30:05 +00:00
< / Button >
< / div >
< / div >
< / div >
) ;
} ) ;
2023-11-07 11:27:21 +00:00
const Composition : FC = observer ( ( ) = > {
2023-07-28 04:30:05 +00:00
return (
< div className = "flex flex-col gap-1 p-2 h-full overflow-hidden" >
< WorkHeader / >
< CompositionPanel / >
< / div >
) ;
} ) ;
2023-11-07 11:27:21 +00:00
export default Composition ;