- web/media_server.py: FastAPI service owning the permanent library (library.json + ./media volume), yt-dlp YouTube Music search, queued download jobs with mp3 transcode (ffmpeg), per-room player desired state, and an HTTPS proxy for external live-stream URLs. - agent/media_mcp.py: Hope tools — search_music, play_song, offer_song_choices, pause/resume/stop/skip/seek, now_playing, list_library, add_stream. - agent/agent.py: media toolset wiring + conversation rule (music playing disables the agent mic; re-engaging pauses music). - web UI: slide-out Music panel (search, suggestions, library, live stream add, play/pause, ±10s, seek, volume) with a reconciling poller and media_state data-channel reports. - token server returns the minted room name; nginx serves /media/files as static (native Range) and proxies /media/api to the media server. - supervisord runs media-server; compose mounts ./media and adds ffmpeg to the image.
240 lines
15 KiB
HTML
240 lines
15 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||
<meta name="theme-color" content="#08111f">
|
||
<title>Hope · Voice companion</title>
|
||
<link rel="manifest" href="manifest.json">
|
||
<link rel="icon" type="image/svg+xml" href="favicon.svg">
|
||
<link rel="stylesheet" href="style.css">
|
||
</head>
|
||
<body data-mode="voice">
|
||
<a class="skip-link" href="#conversation">Skip to conversation</a>
|
||
|
||
<main class="app-shell">
|
||
<header class="topbar">
|
||
<div class="brand">
|
||
<span class="brand-glyph" aria-hidden="true"><i></i></span>
|
||
<div>
|
||
<h1>Hope</h1>
|
||
<p>Voice and text companion</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="topbar-actions">
|
||
<div class="mode-switch" role="group" aria-label="Conversation mode">
|
||
<button id="voiceModeBtn" class="mode-option active" type="button" aria-pressed="true">
|
||
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"></path><path d="M19 10v2a7 7 0 0 1-14 0v-2"></path></svg>
|
||
<span>Voice</span>
|
||
</button>
|
||
<button id="textModeBtn" class="mode-option" type="button" aria-pressed="false">
|
||
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M21 15a4 4 0 0 1-4 4H8l-5 3V7a4 4 0 0 1 4-4h10a4 4 0 0 1 4 4Z"></path></svg>
|
||
<span>Text</span>
|
||
</button>
|
||
</div>
|
||
<div class="identity-chip" aria-label="Current profile: Shane">
|
||
<span class="presence-dot" aria-hidden="true"></span>
|
||
<span>Private to <strong>Shane</strong></span>
|
||
</div>
|
||
<button id="tasksBtn" class="btn btn-secondary tasks-trigger" aria-label="Background tasks" title="Background tasks">
|
||
<svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8 6h11M8 12h11M8 18h7"></path><path d="m3 6 1 1 2-2M3 12l1 1 2-2M3 18l1 1 2-2"></path></svg>
|
||
<span>Tasks</span>
|
||
<span class="tasks-badge" id="tasksBadge" hidden>0</span>
|
||
</button>
|
||
<button id="mediaBtn" class="btn btn-secondary tasks-trigger" aria-label="Music player" title="Music player">
|
||
<svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 18V5l12-2v13"></path><circle cx="6" cy="18" r="3"></circle><circle cx="18" cy="16" r="3"></circle></svg>
|
||
<span>Music</span>
|
||
<span class="tasks-badge" id="mediaBadge" hidden>♫</span>
|
||
</button>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="workspace">
|
||
<section class="voice-console" aria-labelledby="voiceHeading">
|
||
<div class="console-intro">
|
||
<h2 id="voiceHeading">I'm here.</h2>
|
||
<p id="modeDescription">Speak naturally. I’ll listen, remember, and answer in real time.</p>
|
||
</div>
|
||
|
||
<div class="orb-stage">
|
||
<div class="orb" id="orb" data-state="idle" aria-hidden="true">
|
||
<div class="orb-orbit orbit-one"></div>
|
||
<div class="orb-orbit orbit-two"></div>
|
||
<div class="orb-core"><span class="orb-shimmer"></span></div>
|
||
<div class="orb-ring"></div>
|
||
</div>
|
||
<div class="orb-labels" aria-live="polite">
|
||
<span class="state-label" id="stateLabel">Ready when you are</span>
|
||
<span class="thinking-chip" id="thinkingChip" hidden>Thinking…</span>
|
||
<span class="barge-hint" id="bargeHint" hidden>Speak to interrupt</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="controls" aria-label="Conversation controls">
|
||
<button id="startBtn" class="btn btn-primary">
|
||
<span class="button-spark" aria-hidden="true"></span>
|
||
Start conversation
|
||
</button>
|
||
<button id="stopBtn" class="btn btn-danger" disabled>End conversation</button>
|
||
<button id="muteBtn" class="btn btn-icon" aria-label="Mute microphone" title="Mute microphone" disabled>
|
||
<svg viewBox="0 0 24 24" width="19" height="19" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"></path><path d="M19 10v2a7 7 0 0 1-14 0v-2"></path><line x1="8" y1="22" x2="16" y2="22"></line></svg>
|
||
</button>
|
||
<button id="settingsBtn" class="btn btn-icon" aria-label="Voice and brain settings" title="Voice and brain settings">
|
||
<svg viewBox="0 0 24 24" width="19" height="19" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h10M18 7h2M4 17h2M10 17h10"></path><circle cx="16" cy="7" r="2"></circle><circle cx="8" cy="17" r="2"></circle></svg>
|
||
</button>
|
||
</div>
|
||
|
||
<div class="connection-strip">
|
||
<div class="status" id="status">Secure connection ready</div>
|
||
<div class="mic-meter" id="micLevel" aria-hidden="true"><span class="mic-meter-fill" id="micMeterFill"></span></div>
|
||
</div>
|
||
|
||
<p class="privacy-note">
|
||
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><rect x="5" y="10" width="14" height="10" rx="2"></rect><path d="M8 10V7a4 4 0 0 1 8 0v3"></path></svg>
|
||
Your memory stays in Shane’s private profile.
|
||
</p>
|
||
</section>
|
||
|
||
<section class="transcript" id="conversation" aria-labelledby="conversationHeading">
|
||
<div class="transcript-header">
|
||
<div>
|
||
<h2 id="conversationHeading">Conversation</h2>
|
||
<p id="transcriptModeLabel">Live voice transcript</p>
|
||
</div>
|
||
<button id="clearBtn" class="btn btn-ghost" title="Clear conversation">Clear</button>
|
||
</div>
|
||
<div id="messages" aria-live="polite"></div>
|
||
<form class="chat-composer" id="chatComposer" aria-label="Send a text message">
|
||
<label class="sr-only" for="chatInput">Message Hope</label>
|
||
<textarea id="chatInput" rows="1" maxlength="2000" placeholder="Message Hope…" autocomplete="off"></textarea>
|
||
<button id="sendChatBtn" class="send-chat" type="submit" disabled aria-label="Send message">
|
||
<svg viewBox="0 0 24 24" width="19" height="19" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m22 2-7 20-4-9-9-4Z"></path><path d="M22 2 11 13"></path></svg>
|
||
</button>
|
||
<span class="composer-hint">Enter to send · Shift+Enter for a new line</span>
|
||
</form>
|
||
</section>
|
||
</div>
|
||
</main>
|
||
|
||
<div class="sheet-backdrop" id="sheetBackdrop" hidden></div>
|
||
<div class="settings-sheet" id="settingsSheet" role="dialog" aria-modal="true" aria-label="Hope settings">
|
||
<div class="sheet-handle" aria-hidden="true"></div>
|
||
<div class="sheet-header">
|
||
<div>
|
||
<h2>Hope’s settings</h2>
|
||
<p>Voice and brain — changes apply to this conversation.</p>
|
||
</div>
|
||
<button id="closeSheetBtn" class="btn btn-icon" aria-label="Close settings">
|
||
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12"></path></svg>
|
||
</button>
|
||
</div>
|
||
<h3 class="sheet-section-title">Voice</h3>
|
||
<div class="voice-list" id="voiceList"></div>
|
||
<h3 class="sheet-section-title">Brain</h3>
|
||
<div class="voice-list" id="modelList"></div>
|
||
<p class="sheet-note" id="modelNote" hidden></p>
|
||
</div>
|
||
|
||
<div class="tasks-panel" id="tasksPanel" aria-label="Background tasks">
|
||
<div class="tasks-header">
|
||
<div>
|
||
<h2>Background tasks</h2>
|
||
<p>Longer work continues here.</p>
|
||
</div>
|
||
<button id="closeTasksBtn" class="btn btn-icon" aria-label="Close background tasks">
|
||
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12"></path></svg>
|
||
</button>
|
||
</div>
|
||
<div class="tasks-list" id="tasksList">
|
||
<div class="tasks-empty">
|
||
<strong>Nothing running</strong>
|
||
<span>Ask Hope to research or compare something.</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="tasks-panel media-panel" id="mediaPanel" aria-label="Music player">
|
||
<div class="tasks-header">
|
||
<div>
|
||
<h2>Music</h2>
|
||
<p>YouTube Music downloads and live streams, streamed to you.</p>
|
||
</div>
|
||
<button id="closeMediaBtn" class="btn btn-icon" aria-label="Close music player">
|
||
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12"></path></svg>
|
||
</button>
|
||
</div>
|
||
|
||
<div class="media-now">
|
||
<img id="mediaArt" class="media-art" alt="" hidden>
|
||
<div class="media-art-placeholder" id="mediaArtPlaceholder" aria-hidden="true">
|
||
<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" aria-hidden="true"><path d="M9 18V5l12-2v13"></path><circle cx="6" cy="18" r="3"></circle><circle cx="18" cy="16" r="3"></circle></svg>
|
||
</div>
|
||
<div class="media-now-info">
|
||
<strong id="mediaTitle">Nothing loaded</strong>
|
||
<span id="mediaChannel"></span>
|
||
<div class="media-progress" id="mediaProgressWrap" hidden>
|
||
<div class="media-progress-fill" id="mediaProgressFill"></div>
|
||
<span class="media-progress-label" id="mediaProgressLabel">Fetching…</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="media-controls" role="group" aria-label="Playback controls">
|
||
<button id="rewBtn" class="btn btn-icon" aria-label="Back 10 seconds" title="Back 10s">
|
||
<svg viewBox="0 0 24 24" width="19" height="19" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M11 17a5 5 0 1 0-5-5"></path><path d="M11 17H6v-5"></path><path d="m19 5-4 4h-3"></path><path d="m15 9 4-4"></path></svg>
|
||
</button>
|
||
<button id="playPauseBtn" class="btn btn-icon media-play" aria-label="Play or pause" title="Play / pause">
|
||
<svg id="playIcon" viewBox="0 0 24 24" width="22" height="22" fill="currentColor" aria-hidden="true"><path d="M8 5v14l11-7Z"></path></svg>
|
||
<svg id="pauseIcon" viewBox="0 0 24 24" width="22" height="22" fill="currentColor" aria-hidden="true" hidden><path d="M7 5h4v14H7zM13 5h4v14h-4z"></path></svg>
|
||
</button>
|
||
<button id="fwdBtn" class="btn btn-icon" aria-label="Forward 10 seconds" title="Forward 10s">
|
||
<svg viewBox="0 0 24 24" width="19" height="19" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M13 17a5 5 0 1 1 5-5"></path><path d="M13 17h5v-5"></path><path d="m5 5 4 4h3"></path><path d="m9 9 4-4"></path></svg>
|
||
</button>
|
||
<button id="stopMediaBtn" class="btn btn-icon" aria-label="Stop" title="Stop">
|
||
<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true"><rect x="6" y="6" width="12" height="12" rx="2"></rect></svg>
|
||
</button>
|
||
</div>
|
||
|
||
<div class="media-seek">
|
||
<span class="media-time" id="mediaCur">0:00</span>
|
||
<input type="range" id="seekBar" min="0" max="1000" value="0" step="1" aria-label="Seek position">
|
||
<span class="media-time" id="mediaDur">0:00</span>
|
||
</div>
|
||
<div class="media-volume">
|
||
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M11 5 6 9H2v6h4l5 4V5Z"></path><path d="M15.5 8.5a5 5 0 0 1 0 7"></path></svg>
|
||
<input type="range" id="volumeBar" min="0" max="100" value="90" aria-label="Volume">
|
||
</div>
|
||
|
||
<h3 class="sheet-section-title">Search YouTube Music</h3>
|
||
<form class="media-search" id="mediaSearchForm">
|
||
<input id="mediaSearchInput" type="search" placeholder="Song, artist, lyrics…" autocomplete="off">
|
||
<button class="btn btn-secondary" type="submit">Find</button>
|
||
</form>
|
||
<div class="media-list" id="mediaResults"></div>
|
||
|
||
<div id="mediaChoicesWrap" hidden>
|
||
<h3 class="sheet-section-title">Hope’s suggestions</h3>
|
||
<div class="media-list" id="mediaChoices"></div>
|
||
</div>
|
||
|
||
<h3 class="sheet-section-title">Library</h3>
|
||
<div class="media-list" id="mediaLibrary">
|
||
<div class="media-empty">Downloaded songs and streams live here.</div>
|
||
</div>
|
||
|
||
<h3 class="sheet-section-title">Add live stream</h3>
|
||
<form class="media-search media-stream-form" id="streamForm">
|
||
<input id="streamName" type="text" placeholder="Station name" autocomplete="off">
|
||
<input id="streamUrl" type="url" placeholder="https://… audio stream URL" required>
|
||
<button class="btn btn-secondary" type="submit">Add</button>
|
||
</form>
|
||
</div>
|
||
|
||
<audio id="mediaAudio" preload="metadata"></audio>
|
||
|
||
<script src="livekit-client.umd.js"></script>
|
||
<script src="app.js"></script>
|
||
</body>
|
||
</html>
|