LAN access: bind 0.0.0.0 + expose LAN URL (capabilities.lan, UI badge)
This commit is contained in:
@@ -12,6 +12,7 @@ import os
|
||||
import sys
|
||||
import json
|
||||
import shutil
|
||||
import socket
|
||||
import functools
|
||||
|
||||
from app.config import settings
|
||||
@@ -326,6 +327,29 @@ def default_soundfont_dirs() -> list:
|
||||
return ["/opt/daw_engine/soundfonts", os.path.expanduser("~/.sf2")]
|
||||
|
||||
|
||||
def _lan_ips() -> list:
|
||||
"""IPv4 của máy trên mạng LAN (bỏ loopback) — để client browser ở máy
|
||||
khác mở app qua địa chỉ này. Stdlib only (không kéo thư viện ngoài)."""
|
||||
ips = []
|
||||
try:
|
||||
# IP ra mạng theo default route — không gửi gói tin thật
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
|
||||
s.connect(("8.8.8.8", 80))
|
||||
ip = s.getsockname()[0]
|
||||
if ip and not ip.startswith("127.") and ip not in ips:
|
||||
ips.append(ip)
|
||||
except Exception:
|
||||
pass
|
||||
# Enumerate thêm các interface khác (multi-NIC)
|
||||
try:
|
||||
for info in socket.getaddrinfo(socket.gethostname(), None, socket.AF_INET):
|
||||
ip = info[4][0]
|
||||
if ip and not ip.startswith("127.") and ip not in ips:
|
||||
ips.append(ip)
|
||||
except Exception:
|
||||
pass
|
||||
return ips
|
||||
|
||||
@functools.lru_cache(maxsize=1)
|
||||
def detect() -> dict:
|
||||
"""Detect 1 lần (cache toàn cục) — kết quả bất biến trong 1 tiến trình."""
|
||||
@@ -359,6 +383,20 @@ def _vst_render_available() -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def _lan_info() -> dict:
|
||||
"""Địa chỉ + port để client LAN mở app. Port: SF_PORT (desktop engine chọn
|
||||
trước, 8000-8010) hoặc 8000 mặc định."""
|
||||
try:
|
||||
port = int(os.environ.get("SF_PORT") or "8000")
|
||||
except ValueError:
|
||||
port = 8000
|
||||
ips = _lan_ips()
|
||||
return {
|
||||
"ips": ips,
|
||||
"port": port,
|
||||
"urls": [f"http://{ip}:{port}" for ip in ips],
|
||||
}
|
||||
|
||||
def capabilities() -> dict:
|
||||
"""Capabilities API — frontend gọi 1 lần lúc boot để bật/tắt tính năng."""
|
||||
d = detect()
|
||||
@@ -378,6 +416,9 @@ def capabilities() -> dict:
|
||||
"platform": d["platform"],
|
||||
"docker": d["docker"],
|
||||
"environment": d["environment"],
|
||||
# LAN access (chỉ standalone): URL để client browser ở máy khác trên
|
||||
# mạng mở app. Docker (container) → bỏ qua (IP container không dùng được).
|
||||
**({"lan": _lan_info()} if not d["docker"] else {}),
|
||||
"features": features,
|
||||
"default_dirs": {
|
||||
"vst": d["default_vst_dirs"],
|
||||
|
||||
@@ -15171,6 +15171,36 @@ const App = () => {
|
||||
})
|
||||
})));
|
||||
}, [bpm]);
|
||||
// LAN access badge (standalone): server bind 0.0.0.0 — client browser ở máy
|
||||
// khác trên mạng mở app qua http://<LAN-IP>:port. Hiện URL trên máy chủ;
|
||||
// client LAN thấy origin đang dùng.
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
let bar = null;
|
||||
let iv = null;
|
||||
const tryShow = () => {
|
||||
const caps = window.SonicRuntime && window.SonicRuntime.capabilities;
|
||||
const lan = caps && caps.lan;
|
||||
if (!lan || !lan.urls || !lan.urls.length) return false;
|
||||
if (document.getElementById('sf-lan-bar')) return true;
|
||||
const hostname = window.location.hostname;
|
||||
const isLocal = hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1';
|
||||
bar = document.createElement('div');
|
||||
bar.id = 'sf-lan-bar';
|
||||
bar.style.cssText = 'position:fixed;top:8px;right:12px;z-index:9999;background:rgba(16,185,129,0.12);border:1px solid #10b981;color:#a7f3d0;font-size:11px;line-height:1.5;padding:6px 10px;border-radius:8px;max-width:min(70vw,420px);cursor:default;';
|
||||
bar.title = 'Client browser ở máy khác trên LAN mở địa chỉ này để làm việc realtime với máy chạy SonicForge';
|
||||
bar.innerHTML = isLocal
|
||||
? '<b>LAN:</b> ' + lan.urls.map(function (u) { return '<a href="' + u + '" target="_blank" style="color:#a7f3d0;text-decoration:underline">' + u + '</a>'; }).join(' · ')
|
||||
: '<b>Kết nối:</b> ' + window.location.origin;
|
||||
document.body.appendChild(bar);
|
||||
return true;
|
||||
};
|
||||
if (!tryShow()) {
|
||||
iv = setInterval(function () { if (mounted && tryShow()) clearInterval(iv); }, 700);
|
||||
setTimeout(function () { if (iv) clearInterval(iv); }, 20000);
|
||||
}
|
||||
return () => { mounted = false; if (iv) clearInterval(iv); if (bar && bar.parentNode) bar.parentNode.removeChild(bar); };
|
||||
}, []);
|
||||
const openPanel = id => {
|
||||
if (id === 'export') setShowExportPanel(true); else if (id === 'ai') setShowAIPanel(true); else if (id === 'python_tools') setShowPythonToolsPanel(true); else if (id === 'selection') setShowSelectionPanel(true);
|
||||
};
|
||||
|
||||
@@ -754,7 +754,10 @@ useEffect(()=>{const oldSpb=prevBpmRef.current?60.0/parseFloat(prevBpmRef.curren
|
||||
if(oldSpb&&selectionFollowsTempo&&selectionStart!==null&&selectionEnd!==null&&selectionEnd>selectionStart){const startBar=selectionStart/oldSpb;const endBar=selectionEnd/oldSpb;if(endBar-startBar>0.01){setSelectionStart(startBar*secondsPerBar);setSelectionEnd(endBar*secondsPerBar);}}prevBpmRef.current=bpm;// Force canvas redraw
|
||||
setCanvasRedrawCount(n=>n+1);// Recalculate item/section durations
|
||||
updateActiveTracks(prev=>prev.map(t=>({...t,midiItems:(t.midiItems||[]).map(m=>{if(m.length_bars)return{...m,duration:m.length_bars*secondsPerBar};if(m.duration){// Legacy item without length_bars: compute bars from current duration/BPM
|
||||
const bars=Math.max(0.25,Math.round(m.duration/secondsPerBar*4)/4);return{...m,length_bars:bars,duration:bars*secondsPerBar};}return m;}),sections:(t.sections||[]).map(s=>{if(s.length_bars)return{...s,duration:s.length_bars*secondsPerBar};if(s.duration){const bars=Math.max(0.25,Math.round(s.duration/secondsPerBar*4)/4);return{...s,length_bars:bars,duration:bars*secondsPerBar};}return s;})})));},[bpm]);const openPanel=id=>{if(id==='export')setShowExportPanel(true);else if(id==='ai')setShowAIPanel(true);else if(id==='python_tools')setShowPythonToolsPanel(true);else if(id==='selection')setShowSelectionPanel(true);};const[instrumentSelectorTrackId,setInstrumentSelectorTrackId]=useState(null);const[synthTrackDropdownId,setSynthTrackDropdownId]=useState(null);const[fxSelectorTrackId,setFxSelectorTrackId]=useState(null);const handleSetTrackFx=(trackId,fxType)=>{updateActiveTracks(prev=>prev.map(t=>t.id===trackId?{...t,fxType}:t));setFxSelectorTrackId(null);setTimeout(()=>lucide.createIcons(),50);};const[instrumentSelectorData,setInstrumentSelectorData]=useState(null);const[instrumentRefreshKey,setInstrumentRefreshKey]=useState(0);const openInstrumentSelector=trackId=>{setInstrumentSelectorTrackId(trackId);setSfPresetSearchQuery('');setInstrumentRefreshKey(k=>k+1);setSfPresets(null);// Fetch instruments fresh from API for all soundfonts
|
||||
const bars=Math.max(0.25,Math.round(m.duration/secondsPerBar*4)/4);return{...m,length_bars:bars,duration:bars*secondsPerBar};}return m;}),sections:(t.sections||[]).map(s=>{if(s.length_bars)return{...s,duration:s.length_bars*secondsPerBar};if(s.duration){const bars=Math.max(0.25,Math.round(s.duration/secondsPerBar*4)/4);return{...s,length_bars:bars,duration:bars*secondsPerBar};}return s;})})));},[bpm]);// LAN access badge (standalone): server bind 0.0.0.0 — client browser ở máy
|
||||
// khác trên mạng mở app qua http://<LAN-IP>:port. Hiện URL trên máy chủ;
|
||||
// client LAN thấy origin đang dùng.
|
||||
useEffect(()=>{let mounted=true;let bar=null;let iv=null;const tryShow=()=>{const caps=window.SonicRuntime&&window.SonicRuntime.capabilities;const lan=caps&&caps.lan;if(!lan||!lan.urls||!lan.urls.length)return false;if(document.getElementById('sf-lan-bar'))return true;const hostname=window.location.hostname;const isLocal=hostname==='localhost'||hostname==='127.0.0.1'||hostname==='::1';bar=document.createElement('div');bar.id='sf-lan-bar';bar.style.cssText='position:fixed;top:8px;right:12px;z-index:9999;background:rgba(16,185,129,0.12);border:1px solid #10b981;color:#a7f3d0;font-size:11px;line-height:1.5;padding:6px 10px;border-radius:8px;max-width:min(70vw,420px);cursor:default;';bar.title='Client browser ở máy khác trên LAN mở địa chỉ này để làm việc realtime với máy chạy SonicForge';bar.innerHTML=isLocal?'<b>LAN:</b> '+lan.urls.map(function(u){return'<a href="'+u+'" target="_blank" style="color:#a7f3d0;text-decoration:underline">'+u+'</a>';}).join(' · '):'<b>Kết nối:</b> '+window.location.origin;document.body.appendChild(bar);return true;};if(!tryShow()){iv=setInterval(function(){if(mounted&&tryShow())clearInterval(iv);},700);setTimeout(function(){if(iv)clearInterval(iv);},20000);}return()=>{mounted=false;if(iv)clearInterval(iv);if(bar&&bar.parentNode)bar.parentNode.removeChild(bar);};},[]);const openPanel=id=>{if(id==='export')setShowExportPanel(true);else if(id==='ai')setShowAIPanel(true);else if(id==='python_tools')setShowPythonToolsPanel(true);else if(id==='selection')setShowSelectionPanel(true);};const[instrumentSelectorTrackId,setInstrumentSelectorTrackId]=useState(null);const[synthTrackDropdownId,setSynthTrackDropdownId]=useState(null);const[fxSelectorTrackId,setFxSelectorTrackId]=useState(null);const handleSetTrackFx=(trackId,fxType)=>{updateActiveTracks(prev=>prev.map(t=>t.id===trackId?{...t,fxType}:t));setFxSelectorTrackId(null);setTimeout(()=>lucide.createIcons(),50);};const[instrumentSelectorData,setInstrumentSelectorData]=useState(null);const[instrumentRefreshKey,setInstrumentRefreshKey]=useState(0);const openInstrumentSelector=trackId=>{setInstrumentSelectorTrackId(trackId);setSfPresetSearchQuery('');setInstrumentRefreshKey(k=>k+1);setSfPresets(null);// Fetch instruments fresh from API for all soundfonts
|
||||
window.SonicAPI.listPlugins().then(async data=>{var sfonts=data.soundfonts||[];if(sfonts.length===0){setSfPresets([]);return;}try{var results=await Promise.all(sfonts.map(function(sf){var sfId=sf.id.replace('sf_','');return window.SonicAPI.listSoundfontInstruments(sfId).then(function(r){return{sf:sf,presets:r.presets||[]};}).catch(function(){return{sf:sf,presets:[]};});}));var all=[];results.forEach(function(r){var sf=r.sf;var sfId=sf.id.startsWith('sf_')?sf.id:'sf_'+sf.id;var sfName=sf.display||sf.name||sf.id;(r.presets||[]).forEach(function(p){all.push({...p,_sfId:sfId,_sfName:sfName,_sfDisplay:sfName.substring(0,30)});});});setSfPresets(all.length>0?all:[]);}catch(e){setSfPresets([]);}}).catch(function(){setSfPresets([]);});};const closeInstrumentSelector=()=>{setInstrumentSelectorTrackId(null);setSynthCategory(null);setSelectedSoundFontId(null);setSfPresetSearchQuery('');};const[synthCategory,setSynthCategory]=useState(null);// 'vst' | 'soundfont'
|
||||
const[selectedSoundFontId,setSelectedSoundFontId]=useState(null);const[sfPresets,setSfPresets]=useState(null);// presets from SoundFont
|
||||
const[instrumentDropdownTrackId,setInstrumentDropdownTrackId]=useState(null);const[instrumentDropdownBtnRect,setInstrumentDropdownBtnRect]=useState(null);const[instrumentSearchQuery,setInstrumentSearchQuery]=useState('');const[sfPresetSearchQuery,setSfPresetSearchQuery]=useState('');const filteredInstruments=useMemo(()=>{if(!instrumentSelectorData||!instrumentSearchQuery)return{soundfonts:instrumentSelectorData?.soundfonts||[],vst:instrumentSelectorData?.vst_instruments||[]};const q=instrumentSearchQuery.toLowerCase();return{soundfonts:(instrumentSelectorData.soundfonts||[]).filter(sf=>(sf.display||sf.name||sf.id).toLowerCase().includes(q)),vst:(instrumentSelectorData.vst_instruments||[]).filter(v=>(v.name||v.id).toLowerCase().includes(q))};},[instrumentSearchQuery,instrumentSelectorData]);useEffect(()=>{window.SonicAPI.listPlugins().then(async data=>{try{const catResp=(await window.SonicAPI.getSoundfontCatalog?.())??(await fetch('/api/v1/plugins/soundfonts/catalog').then(r=>r.json()));const catalog=catResp.full_catalog||{};data.soundfonts=(data.soundfonts||[]).map(sf=>{const sfId=sf.id.replace('sf_','');const catEntry=catalog[sfId.toLowerCase()]||catalog[sfId];if(catEntry&&catEntry.instruments){return{...sf,presets:catEntry.instruments};}return sf;});}catch(e){console.warn('Catalog fetch error:',e);}setInstrumentSelectorData(data);}).catch(()=>{});},[instrumentRefreshKey]);useEffect(()=>{if(!instrumentDropdownTrackId)return;const handler=e=>{setInstrumentDropdownTrackId(null);setInstrumentDropdownBtnRect(null);setInstrumentSearchQuery('');};document.addEventListener('click',handler);return()=>document.removeEventListener('click',handler);},[instrumentDropdownTrackId]);const GM_INSTRUMENTS=["Acoustic Grand Piano","Bright Acoustic Piano","Electric Grand Piano","Honky-tonk Piano","Electric Piano 1","Electric Piano 2","Harpsichord","Clavi","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 Voice","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","Bag pipe","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"];const setTrackInstrumentWithProgram=(trackId,instrumentId,programNumber,displayName,bankNumber)=>{const isSfInstrument=instrumentId&&typeof instrumentId==='string'&&instrumentId.startsWith('sf_');const sfBank=bankNumber!==undefined?bankNumber:isSfInstrument?0:undefined;const sfProg=programNumber!==undefined?programNumber:undefined;var mt=activeTracksRef.current||tracks;var curTrk=null;for(var ci=0;ci<mt.length;ci++){if(mt[ci].id===trackId){curTrk=mt[ci];break;}}// ── Unload Carla bridge khi chuyển từ VSTi sang instrument KHÔNG phải VST ──
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
<script src="/static/js/services/midiExtractor.js?v=202607281052"></script>
|
||||
<script src="/static/js/services/promptTemplateManager.js?v=202607281039"></script>
|
||||
<script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script>
|
||||
<script src="/static/js/app.precompiled.js?v=202608101914" defer></script>
|
||||
<script src="/static/js/app.precompiled.js?v=202608102044" defer></script>
|
||||
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
|
||||
<style>
|
||||
:root {
|
||||
|
||||
Reference in New Issue
Block a user