codebook / potato /templates /generated /Audio-Segmentation-with-Context-base_template_v2.html
davidjurgens's picture
Deploy: Potato — Codebook Annotation
aceb1b2 verified
Raw
History Blame Contribute Delete
60.7 kB
<!-- CONFIG_HASH: 3c45f7a3bb2ffffb6adf9f38fbeb17e0 -->
{# Default ui_lang values if not provided by context processor #}
{% if ui_lang is not defined %}
{% set ui_lang = {'next_button': 'Next', 'previous_button': 'Previous', 'labeled_badge': 'Labeled', 'not_labeled_badge': 'Not labeled', 'submit_button': 'Submit', 'progress_label': 'Progress', 'go_button': 'Go', 'logout': 'Logout', 'loading': 'Loading annotation interface...', 'error_heading': 'Error', 'retry_button': 'Retry', 'adjudicate': 'Adjudicate', 'codebook': 'Codebook', 'instructions_heading': 'Instructions', 'text_to_annotate': 'Text to Annotate:', 'video_to_annotate': 'Video to Annotate:', 'audio_to_annotate': 'Audio to Annotate:', 'powered_by': 'Powered by', 'cite_us': 'Cite Us', 'html_lang': 'en', 'html_dir': 'ltr'} %}
{% endif %}
<!DOCTYPE html>
<html lang="{{ ui_lang.html_lang|default('en') }}" dir="{{ ui_lang.html_dir|default('ltr') }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Audio Segmentation with Context</title>
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<!-- Custom Styles -->
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
<!-- Span Annotation Styles -->
<link rel="stylesheet" href="{{ url_for('static', filename='span-styles.css') }}">
{% set frontend_assets = frontend_assets | default({}) %}
<!-- Image Annotation Styles -->
{% if frontend_assets.image_annotation | default(false) %}
<link rel="stylesheet" href="{{ url_for('static', filename='image-annotation.css') }}?v=2">
{% endif %}
<!-- Audio Annotation Styles -->
{% if frontend_assets.audio_annotation | default(false) %}
<link rel="stylesheet" href="{{ url_for('static', filename='audio-annotation.css') }}?v=2">
{% endif %}
<!-- Video Annotation Styles -->
{% if frontend_assets.video_annotation | default(false) %}
<link rel="stylesheet" href="{{ url_for('static', filename='video-annotation.css') }}">
{% endif %}
<!-- Span Link Styles -->
{% if frontend_assets.span_link | default(false) %}
<link rel="stylesheet" href="{{ url_for('static', filename='span-link-styles.css') }}">
{% endif %}
<!-- Event Annotation Styles -->
{% if frontend_assets.event_annotation | default(false) %}
<link rel="stylesheet" href="{{ url_for('static', filename='event-annotation.css') }}">
{% endif %}
<!-- Spreadsheet Display Styles -->
<link rel="stylesheet" href="{{ url_for('static', filename='spreadsheet-display.css') }}">
<!-- Coding Trace Display Styles -->
<link rel="stylesheet" href="{{ url_for('static', filename='coding-trace.css') }}">
<!-- Live Coding Agent Viewer -->
{% if frontend_assets.live_coding_agent | default(false) %}
<script src="{{ url_for('static', filename='live-coding-agent-viewer.js') }}" defer></script>
{% endif %}
<!-- Format Display Styles (document, code, etc.) -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/format-displays.css') }}">
<!-- Document Bounding Box Styles -->
{% if frontend_assets.document_bbox | default(false) %}
<link rel="stylesheet" href="{{ url_for('static', filename='document-bbox.css') }}">
{% endif %}
<!-- PDF Annotation Styles -->
{% if frontend_assets.pdf_bbox | default(false) %}
<link rel="stylesheet" href="{{ url_for('static', filename='pdf-annotation.css') }}">
{% endif %}
<!-- Display Logic Styles (conditional schema branching) -->
<link rel="stylesheet" href="{{ url_for('static', filename='display-logic.css') }}">
<!-- Coreference Chain Styles -->
{% if frontend_assets.coreference | default(false) %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/coreference.css') }}">
{% endif %}
<!-- Segmentation Tool Styles -->
{% if frontend_assets.segmentation_tools | default(false) %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/segmentation.css') }}">
{% endif %}
<!-- Conversation Tree Styles -->
{% if frontend_assets.conversation_tree | default(false) %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/conversation-tree.css') }}">
{% endif %}
<!-- Video Tracking Styles -->
{% if frontend_assets.tracking | default(false) %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/tracking.css') }}">
{% endif %}
<!-- Triage Styles -->
{% if frontend_assets.triage | default(false) %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/triage.css') }}">
{% endif %}
<!-- Entity Linking Styles (knowledge base linking) -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/entity-linking.css') }}">
<!-- Tiered Annotation Styles (ELAN-style hierarchical annotation) -->
{% if frontend_assets.tiered_annotation | default(false) %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/tiered-annotation.css') }}">
{% endif %}
{% if agent_proxy_enabled | default(false) %}
<!-- Agent Chat Styles -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/agent-chat.css') }}">
{% endif %}
{% if chat_enabled | default(false) %}
<!-- LLM Chat Sidebar Styles -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/llm-chat-sidebar.css') }}?v=1">
{% endif %}
<!-- Universal Memos sidebar styles -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/memos.css') }}?v=2">
<link rel="stylesheet" href="{{ url_for('static', filename='css/search.css') }}?v=2">
<link rel="stylesheet" href="{{ url_for('static', filename='css/codebook.css') }}?v=10">
<!-- jQuery (required for span annotation) -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
<!-- Project-level base CSS (injected from base_css config option) -->
{{ PROJECT_BASE_CSS | safe }}
</head>
<body>
<!-- Header navigation bar -->
<nav class="potato-navbar" role="navigation">
<div class="navbar-inner">
<div class="navbar-brand">
{% if header_logo_url %}
<img class="header-logo" src="{{ header_logo_url }}" alt="Logo">
{% endif %}
<span class="task-name">Audio Segmentation with Context</span>
</div>
<div class="navbar-center">
<div class="annotation-status-indicator" id="annotation-status">
<span class="status-badge {{ annotation_status }}">
{% if annotation_status == 'labeled' %}
{{ ui_lang.labeled_badge }}
{% elif annotation_status == 'in_progress' %}
{{ ui_lang.in_progress_badge }}
{% else %}
{{ ui_lang.not_labeled_badge }}
{% endif %}
</span>
</div>
<div class="navbar-divider"></div>
<div class="progress-section">
<span class="progress-label">{{ ui_lang.progress_label }}</span>
<span class="fw-medium" id="progress-counter">{{finished}}/{{total_count}}</span>
</div>
{% if ibws_round_info %}
<div class="navbar-divider"></div>
<div class="ibws-round-banner" id="ibws-round-banner">
<span class="ibws-round-label">Round {{ ibws_round_info.current_round }}{% if ibws_round_info.max_rounds %} of {{ ibws_round_info.max_rounds }}{% endif %}</span>
<span class="ibws-round-detail">{{ ibws_round_info.terminal_items }}/{{ ibws_round_info.total_items }} ranked</span>
</div>
{% endif %}
<div class="navbar-divider"></div>
{% if instance_index is defined %}
<div class="instance-number-section">
<span class="instance-number-label">Instance</span>
<span class="fw-medium" id="instance-number">#{{ instance_index|int + 1 }}</span>
</div>
{% endif %}
{% if not jumping_to_id_disabled %}
<div class="navbar-divider"></div>
<div class="nav-controls">
<button type="button" class="nav-control-btn" id="jump-unannotated-prev-btn"
onclick="jumpToUnannotatedPrev()" title="Previous unannotated">
<i class="fas fa-backward"></i>
</button>
<button type="button" class="nav-control-btn" id="jump-unannotated-btn"
onclick="jumpToUnannotated()" title="Next unannotated">
<i class="fas fa-forward"></i>
</button>
</div>
<div class="navbar-divider"></div>
<div class="goto-section">
<input type="number" id="go_to" placeholder="#"
value="" min="0" max="{{total_count}}" required
onfocusin="user_input()" onfocusout="user_input_leave()">
<button type="button" class="goto-btn" id="go-to-btn">{{ ui_lang.go_button }}</button>
</div>
{% endif %}
</div>
<div class="navbar-end">
{% if is_adjudicator %}
<a href="/adjudicate" class="adjudicate-btn" title="Adjudication Mode">
<i class="fas fa-gavel"></i> {{ ui_lang.adjudicate }}
</a>
{% endif %}
{% if annotation_codebook_url %}
<a href="{{ annotation_codebook_url | replace('data_files/', '/media/') }}"
class="codebook-btn"
target="_blank"
rel="noopener noreferrer"
title="Open annotation codebook">
<i class="fas fa-book-open"></i> {{ ui_lang.codebook }}
</a>
{% endif %}
<div class="user-pill">
<span class="username" id="username-display">{{username}}</span>
<a href="/logout" class="logout-btn">{{ ui_lang.logout }}</a>
</div>
</div>
</div>
</nav>
<div class="container-fluid">
<!-- Main annotation area -->
<div id="task_layout" class="shadcn-card mb-2 {% if has_image_annotation or has_video_annotation or has_audio_annotation %}media-annotation{% else %}text-annotation{% endif %}">
<div class="shadcn-card-content">
<!-- Loading state -->
<div id="loading-state" class="text-center py-5">
<div class="loading-spinner mb-3"></div>
<p>{{ ui_lang.loading }}</p>
</div>
<!-- Error state -->
<div id="error-state" class="error-message" style="display: none;">
<h5><i class="fas fa-exclamation-triangle me-2"></i>{{ ui_lang.error_heading }}</h5>
<p id="error-message-text"></p>
<button id="error-retry-btn" class="shadcn-button shadcn-button-primary" onclick="loadCurrentInstance()">
<i class="fas fa-redo me-2"></i>{{ ui_lang.retry_button }}
</button>
<a id="error-done-link" href="/done" class="shadcn-button shadcn-button-primary" style="display: none;">
<i class="fas fa-check me-2"></i>Finish
</a>
</div>
<!-- Main content area -->
<div id="main-content" style="display: none;">
{% if annotation_instructions | default('') %}
<details class="annotation-instructions-banner" open>
<summary>{{ ui_lang.instructions_heading|default('Instructions') }}</summary>
<div class="annotation-instructions-content">
{{ annotation_instructions | safe }}
</div>
</details>
{% endif %}
<!-- Instance display area -->
<div class="mb-4">
{% if has_instance_display | default(false) %}
<!-- New instance_display mode: explicit content display configuration -->
{{ display_html | safe }}
<!-- Hidden element for legacy JS compatibility -->
<div id="instance-text" style="display: none;">
<div id="text-content" data-original-text="{{instance_plain_text | sanitize_html}}">{{instance | sanitize_html}}</div>
</div>
{% elif has_video_annotation | default(false) %}
<h5 class="mb-3">{{ ui_lang.video_to_annotate|default('Video to Annotate:') }}</h5>
<!-- For video annotation, keep the text-content element accessible for JS but visually hidden -->
<div id="instance-text" class="instance-text-container" style="position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0, 0, 0, 0);">
<div id="text-content" data-original-text="{{instance_plain_text | sanitize_html}}">{{instance | sanitize_html}}</div>
</div>
{% elif has_audio_annotation | default(false) %}
<h5 class="mb-3">{{ ui_lang.audio_to_annotate|default('Audio to Annotate:') }}</h5>
<!-- For audio annotation, keep the text-content element accessible for JS but visually hidden -->
<div id="instance-text" class="instance-text-container" style="position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0, 0, 0, 0);">
<div id="text-content" data-original-text="{{instance_plain_text | sanitize_html}}">{{instance | sanitize_html}}</div>
</div>
{% elif has_image_annotation | default(false) %}
<!-- For image annotation, hide this section - the image is loaded directly into the annotation canvas -->
<!-- Store the image URL in a hidden element for JavaScript to access -->
<div id="instance-text" style="display: none;">
<div id="text-content" data-original-text="{{instance_plain_text | sanitize_html}}" data-image-url="{{instance | sanitize_html}}">{{instance | sanitize_html}}</div>
</div>
{% else %}
{% if is_annotation_page %}
{% if ui_lang.text_to_annotate|default('Text to Annotate:') %}
<h5 class="instance-text-heading">{{ ui_lang.text_to_annotate|default('Text to Annotate:') }}</h5>
{% endif %}
<div id="instance-text" class="p-3 border rounded instance-text-container" style="background-color: var(--light-bg); position: relative;">
<div id="text-content" data-original-text="{{instance_plain_text | sanitize_html}}" style="position: relative; z-index: 1; pointer-events: auto;">{{instance | sanitize_html}}
<div id="span-overlays" style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; pointer-events: none; z-index: 2;">
</div>
</div>
</div>
{% else %}
<div id="instance-text" style="display: none;">
<div id="text-content" data-original-text=""></div>
</div>
{% endif %}
{% endif %}
</div>
<!-- Status messages -->
<div id="status" class="status" style="display: none;"></div>
<!-- Hidden input for instance_id -->
<input type="hidden" id="instance_id" name="instance_id" value="{{instance_id}}">
<!-- Full instance record as JSON, consumed by schemas that
bind to structured instance fields (process_reward,
trajectory_eval) via document.querySelector('[data-instance-json]').
tojson escapes single quotes so the single-quoted
attribute is safe even when content contains apostrophes. -->
<div id="instance-json-data" style="display: none;"
data-instance-json='{{ (instance_record if instance_record is defined and instance_record else {}) | tojson }}'></div>
<!-- Annotation forms -->
<div id="annotation-forms">
<!-- CONFIG_HASH: 3c45f7a3bb2ffffb6adf9f38fbeb17e0_d135eb1ca0884ffd46c2b898b80065c5 -->
<!-- Generated annotation layout file -->
<!-- This file was automatically generated based on the annotation schemes in your config -->
<!-- You can customize this file to modify the layout of your annotation interface -->
<!-- Changes to this file will be preserved across server restarts -->
<div class="annotation_schema">
<form id="audio_segments" class="annotation-form audio-annotation" action="javascript:void(0)" data-source-field="audio_url">
<fieldset schema="audio_segments">
<legend>Select segments in the audio and label them</legend>
<!-- Audio Annotation Container -->
<div class="audio-annotation-container" data-schema="audio_segments">
<!-- 1. Full waveform overview at top -->
<div class="waveform-wrapper">
<div class="overview-label" style="font-size: 0.85em; color: #666; margin-bottom: 4px;">Full Audio Overview (click to navigate)</div>
<div id="overview-audio_segments" class="overview-container" style="margin-bottom: 12px;"></div>
<!-- 2. Zoomed waveform below -->
<div class="zoomview-label" style="font-size: 0.85em; color: #666; margin-bottom: 4px;">Zoomed View (right-click drag to annotate)</div>
<div id="waveform-audio_segments" class="waveform-container"></div>
</div>
<!-- 3. Toolbar with annotation buttons -->
<div class="audio-annotation-toolbar" style="margin-top: 12px;">
<!-- Playback controls -->
<div class="playback-group">
<button type="button" class="playback-btn" data-action="play" title="Play or pause audio playback. Keyboard shortcut: Space">
<span class="play-icon"></span>
<span class="pause-icon" style="display:none;"></span>
</button>
<button type="button" class="playback-btn" data-action="stop" title="Stop playback and return to the beginning"></button>
<span class="time-display" title="Current playback position / Total duration">
<span class="current-time">0:00</span> / <span class="total-time">0:00</span>
</span>
</div>
<div class="playback-rate-group">
<span class="tool-group-label" title="Adjust playback speed">Speed:</span>
<select class="playback-rate-select" title="Change audio playback speed (0.5x to 2x)">
<option value="0.5">0.5x</option>
<option value="0.75">0.75x</option>
<option value="1" selected>1x</option>
<option value="1.25">1.25x</option>
<option value="1.5">1.5x</option>
<option value="2">2x</option>
</select>
</div>
<!-- Label selector (for label/both modes) -->
<div class="label-group">
<span class="tool-group-label" title="Select a label before creating segments">Label:</span>
<button type="button" class="label-btn" data-label="intro" data-color="#4ECDC4" title="Select 'intro' as the label for new segments." style="--label-color: #4ECDC4;"><span class="label-color-dot" style="background-color: #4ECDC4;"></span>intro</button><button type="button" class="label-btn" data-label="verse" data-color="#FF6B6B" title="Select 'verse' as the label for new segments." style="--label-color: #FF6B6B;"><span class="label-color-dot" style="background-color: #FF6B6B;"></span>verse</button><button type="button" class="label-btn" data-label="chorus" data-color="#45B7D1" title="Select 'chorus' as the label for new segments." style="--label-color: #45B7D1;"><span class="label-color-dot" style="background-color: #45B7D1;"></span>chorus</button><button type="button" class="label-btn" data-label="bridge" data-color="#96CEB4" title="Select 'bridge' as the label for new segments." style="--label-color: #96CEB4;"><span class="label-color-dot" style="background-color: #96CEB4;"></span>bridge</button><button type="button" class="label-btn" data-label="outro" data-color="#FFEAA7" title="Select 'outro' as the label for new segments." style="--label-color: #FFEAA7;"><span class="label-color-dot" style="background-color: #FFEAA7;"></span>outro</button>
</div>
<!-- Zoom controls -->
<div class="zoom-group">
<button type="button" class="zoom-btn" data-action="zoom-in" title="Zoom in to see more detail. Keyboard shortcut: + or =">+</button>
<button type="button" class="zoom-btn" data-action="zoom-out" title="Zoom out to see more of the audio. Keyboard shortcut: -">-</button>
<button type="button" class="zoom-btn" data-action="zoom-fit" title="Fit the entire audio in the view. Keyboard shortcut: 0">Fit</button>
</div>
<!-- Segment controls -->
<div class="segment-group">
<button type="button" class="segment-btn" data-action="set-start" title="Mark the start of a new segment at the current playback position. Keyboard shortcut: [">[</button>
<button type="button" class="segment-btn" data-action="set-end" title="Mark the end of a new segment at the current playback position. Keyboard shortcut: ]">]</button>
<button type="button" class="segment-btn" data-action="create-segment" title="Create a segment from the marked start and end positions. Keyboard shortcut: Enter">+ Segment</button>
<button type="button" class="segment-btn delete-btn" data-action="delete-segment" title="Delete the currently selected segment. Keyboard shortcut: Delete or Backspace" disabled>Delete</button>
</div>
<!-- Segment count -->
<div class="count-group">
<span class="segment-count" title="Number of segments you have created">Segments: <span class="count-value">0</span></span>
</div>
<!-- Help toggle button -->
<div class="help-toggle" style="margin-left: auto;">
<button type="button" class="help-toggle-btn" onclick="this.closest('.audio-annotation-container').querySelector('.help-panel').classList.toggle('collapsed')" title="Show or hide usage instructions" style="background: none; border: 1px solid #ccc; border-radius: 4px; padding: 4px 8px; cursor: pointer; font-size: 0.85em;">
❓ Help
</button>
</div>
</div>
<!-- 4. Help/Instructions Panel -->
<div class="help-panel" style="background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 4px; padding: 12px; margin-top: 10px; font-size: 0.9em;">
<div style="display: flex; gap: 30px; flex-wrap: wrap;">
<div>
<strong style="color: #495057;">🖱️ Creating Segments:</strong>
<ul style="margin: 5px 0 0 0; padding-left: 20px; color: #666;">
<li><strong>Left-click</strong> to navigate through the audio</li>
<li><strong>Right-click + drag</strong> to create a segment</li>
<li>Or use <strong>[ and ]</strong> keys to mark start/end while playing</li>
<li><strong>Drag segment edges</strong> to resize</li>
</ul>
</div>
<div>
<strong style="color: #495057;">⌨️ Keyboard Shortcuts:</strong>
<ul style="margin: 5px 0 0 0; padding-left: 20px; color: #666;">
<li><strong>Space</strong> - Play/Pause</li>
<li><strong>[ ]</strong> - Mark segment start/end</li>
<li><strong>Enter</strong> - Create segment from markers</li>
<li><strong>Delete</strong> - Delete selected segment</li>
<li><strong>+/-/0</strong> - Zoom in/out/fit</li>
<li><strong>← →</strong> - Skip 5s (Shift: 30s)</li>
</ul>
</div>
<div>
<strong style="color: #495057;">📝 Quick Start:</strong>
<ol style="margin: 5px 0 0 0; padding-left: 20px; color: #666;">
<li>Select a label above</li>
<li>Right-click and drag on waveform to create segment</li>
<li>Adjust edges by dragging if needed</li>
</ol>
</div>
</div>
</div>
<style>
.help-panel.collapsed { display: none; }
</style>
<!-- Segment list -->
<div class="segment-list-container" style="margin-top: 12px;">
<h4>Segments</h4>
<div id="segment-list-audio_segments" class="segment-list"></div>
</div>
<!-- Segment questions panel (for questions/both modes) -->
<div id="segment-questions-audio_segments" class="segment-questions-panel" style="display: none;">
<h4>Segment Details</h4>
<div class="segment-questions-content"></div>
</div>
<!-- Hidden input for storing annotation data -->
<input type="hidden"
name="audio_segments"
id="input-audio_segments"
class="annotation-data-input"
value="">
<!-- Hidden audio element -->
<audio id="audio-audio_segments" style="display: none;"></audio>
</div>
<!-- Initialize the annotation manager -->
<script>
(function() {
console.log('[AudioAnnotation] Initialization script started for schema: audio_segments');
function initWhenReady() {
if (typeof AudioAnnotationManager === 'undefined') {
console.log('[AudioAnnotation] Waiting for AudioAnnotationManager...');
setTimeout(initWhenReady, 100);
return;
}
// Peaks.js exports as lowercase 'peaks', create alias for uppercase 'Peaks' if needed
if (typeof Peaks === 'undefined' && typeof peaks !== 'undefined') {
window.Peaks = peaks;
console.log('[AudioAnnotation] Created Peaks alias from lowercase peaks');
}
if (typeof Peaks === 'undefined' && typeof peaks === 'undefined') {
console.log('[AudioAnnotation] Waiting for Peaks.js (checked both Peaks and peaks)...');
setTimeout(initWhenReady, 100);
return;
}
console.log('[AudioAnnotation] AudioAnnotationManager and Peaks.js loaded');
var container = document.querySelector('.audio-annotation-container[data-schema="audio_segments"]');
if (!container) {
console.error('[AudioAnnotation] Container not found for schema: audio_segments');
return;
}
// Wait for waveform container to be visible and have dimensions
// (main-content starts hidden and is shown after API data loads)
var waveformContainer = document.getElementById('waveform-audio_segments');
if (waveformContainer) {
var rect = waveformContainer.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) {
console.log('[AudioAnnotation] Waveform container not visible yet, waiting...');
setTimeout(initWhenReady, 100);
return;
}
}
console.log('[AudioAnnotation] Container found and visible:', container);
var config = {"schemaName": "audio_segments", "mode": "label", "labels": [{"name": "intro", "color": "#4ECDC4", "key_value": null}, {"name": "verse", "color": "#FF6B6B", "key_value": null}, {"name": "chorus", "color": "#45B7D1", "key_value": null}, {"name": "bridge", "color": "#96CEB4", "key_value": null}, {"name": "outro", "color": "#FFEAA7", "key_value": null}], "segmentSchemes": [], "minSegments": 1, "maxSegments": null, "zoomEnabled": true, "playbackRateControl": true, "sourceField": "audio_url", "waveform": true, "spectrogram": false, "spectrogramOptions": {"fft_size": 2048, "hop_length": 512, "frequency_range": [0, 8000], "color_map": "viridis"}};
var waveformId = 'waveform-audio_segments';
var overviewId = 'overview-audio_segments';
var audioId = 'audio-audio_segments';
var inputId = 'input-audio_segments';
var segmentListId = 'segment-list-audio_segments';
var questionsId = 'segment-questions-audio_segments';
var spectrogramId = config.spectrogram ? 'spectrogram-audio_segments' : null;
var spectrogramCanvasId = config.spectrogram ? 'spectrogram-canvas-audio_segments' : null;
var spectrogramPlayheadId = config.spectrogram ? 'spectrogram-playhead-audio_segments' : null;
// Get audio URL from various sources
var audioUrl = null;
// 1. First try to find from source_field in instance_display field
if (config.sourceField) {
var displayField = document.querySelector('[data-field-key="' + config.sourceField + '"]');
console.log('[AudioAnnotation] Looking for display field:', config.sourceField, displayField);
if (displayField) {
// Check for data-source-url attribute
var sourceUrl = displayField.dataset.sourceUrl;
if (sourceUrl) {
audioUrl = sourceUrl;
console.log('[AudioAnnotation] Found audio URL from display field data-source-url:', audioUrl);
}
// Check for audio element inside display field
var audioEl = displayField.querySelector('audio');
if (!audioUrl && audioEl) {
var source = audioEl.querySelector('source');
audioUrl = source ? source.src : audioEl.src;
console.log('[AudioAnnotation] Found audio URL from display field audio element:', audioUrl);
}
}
}
// 2. Try instance-display container's data-instance-fields attribute
if (!audioUrl && config.sourceField) {
var instanceDisplayContainer = document.querySelector('.instance-display-container[data-instance-fields]');
if (instanceDisplayContainer) {
try {
var instanceFields = JSON.parse(instanceDisplayContainer.dataset.instanceFields);
if (instanceFields[config.sourceField]) {
audioUrl = instanceFields[config.sourceField];
console.log('[AudioAnnotation] Found audio URL from instance fields:', audioUrl);
}
} catch (e) {
console.warn('[AudioAnnotation] Failed to parse instance fields:', e);
}
}
}
// 3. Fallback: Try instance-text or text-content containers
if (!audioUrl) {
var instanceContainer = document.getElementById('instance-text') || document.getElementById('text-content');
console.log('[AudioAnnotation] Fallback - Instance container:', instanceContainer);
if (instanceContainer) {
var text = instanceContainer.textContent || instanceContainer.innerText;
// Check if it's a URL
if (text && (text.trim().startsWith('http') || text.trim().endsWith('.mp3') || text.trim().endsWith('.wav'))) {
audioUrl = text.trim();
console.log('[AudioAnnotation] Found audio URL in text:', audioUrl);
}
// Check for audio element
var audioEl = instanceContainer.querySelector('audio');
if (audioEl && audioEl.src) {
audioUrl = audioEl.src;
console.log('[AudioAnnotation] Found audio URL in audio element:', audioUrl);
}
}
}
// Initialize manager
console.log('[AudioAnnotation] Creating AudioAnnotationManager with audioUrl:', audioUrl);
var manager = new AudioAnnotationManager({
container: container,
waveformId: waveformId,
overviewId: overviewId,
audioId: audioId,
inputId: inputId,
segmentListId: segmentListId,
questionsId: questionsId,
spectrogramId: spectrogramId,
spectrogramCanvasId: spectrogramCanvasId,
spectrogramPlayheadId: spectrogramPlayheadId,
config: config
});
// Store reference on container
container.audioAnnotationManager = manager;
// Load audio if available
if (audioUrl) {
// Use proxy for external URLs to avoid CORS issues
var finalAudioUrl = audioUrl;
if (audioUrl.startsWith('http://') || audioUrl.startsWith('https://')) {
// Check if it's an external URL (not same origin)
var currentOrigin = window.location.origin;
if (!audioUrl.startsWith(currentOrigin)) {
finalAudioUrl = '/api/audio/proxy?url=' + encodeURIComponent(audioUrl);
console.log('[AudioAnnotation] Using proxy for external URL:', finalAudioUrl);
}
}
console.log('[AudioAnnotation] Calling loadAudio with:', finalAudioUrl);
manager.loadAudio(finalAudioUrl);
} else {
console.warn('[AudioAnnotation] No audio URL found, waveform will not be loaded');
}
// Wire up toolbar buttons
container.querySelectorAll('.playback-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
var action = this.dataset.action;
if (action === 'play') manager.togglePlayPause();
else if (action === 'stop') manager.stop();
});
});
container.querySelectorAll('.label-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
var label = this.dataset.label;
var color = this.dataset.color;
manager.setActiveLabel(label, color);
container.querySelectorAll('.label-btn').forEach(function(b) {
b.classList.remove('active');
});
this.classList.add('active');
});
});
container.querySelectorAll('.zoom-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
var action = this.dataset.action;
if (action === 'zoom-in') manager.zoomIn();
else if (action === 'zoom-out') manager.zoomOut();
else if (action === 'zoom-fit') manager.zoomToFit();
});
});
container.querySelectorAll('.segment-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
var action = this.dataset.action;
if (action === 'set-start') manager.setSelectionStart();
else if (action === 'set-end') manager.setSelectionEnd();
else if (action === 'create-segment') manager.createSegmentFromSelection();
else if (action === 'delete-segment') manager.deleteSelectedSegment();
});
});
// Playback rate control
var rateSelect = container.querySelector('.playback-rate-select');
if (rateSelect) {
rateSelect.addEventListener('change', function() {
manager.setPlaybackRate(parseFloat(this.value));
});
}
// Set default label
var firstLabelBtn = container.querySelector('.label-btn');
if (firstLabelBtn) firstLabelBtn.click();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initWhenReady);
} else {
initWhenReady();
}
})();
</script>
</fieldset>
</form>
</div>
</div>
<!-- Structured data elements (var_elems) -->
{{ var_elems | safe }}
</div>
</div>
</div>
<!-- Navigation buttons -->
<div class="potato-nav">
{% if can_go_back | default(true) %}
<button class="shadcn-button shadcn-button-outline" id="prev-btn" onclick="navigateToPrevious()">
<i class="fas fa-arrow-left me-2"></i>{{ ui_lang.previous_button }}
</button>
{% else %}
<div id="prev-btn-placeholder"></div>
{% endif %}
<button class="shadcn-button shadcn-button-primary" id="next-btn" onclick="navigateToNext()">
{{ ui_lang.next_button }}<i class="fas fa-arrow-right ms-2"></i>
</button>
</div>
{% if custom_footer_html | default('') %}
<!-- Custom footer (e.g., HF Space promotional banner) -->
{{ custom_footer_html | safe }}
{% endif %}
<!-- Footer -->
<footer class="potato-footer">
{{ ui_lang.powered_by|default('Powered by') }} <a href="https://potatoannotator.com/" target="_blank" rel="noopener noreferrer">Potato</a>
<span class="footer-sep">&middot;</span>
<a href="https://github.com/davidjurgens/potato" target="_blank" rel="noopener noreferrer">GitHub</a>
<span class="footer-sep">&middot;</span>
<a href="https://github.com/davidjurgens/potato#cite-us" target="_blank" rel="noopener noreferrer">{{ ui_lang.cite_us|default('Cite Us') }}</a>
</footer>
</div>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<!-- Configuration from server -->
<script type="text/javascript">
window.config = {
annotation_task_name: {{ annotation_task_name | tojson }},
is_annotation_page: {{ 'true' if is_annotation_page else 'false' }},
debug: {{ 'true' if debug_mode else 'false' }},
ui_debug: {{ 'true' if ui_debug else 'false' }},
server_debug: {{ 'true' if server_debug else 'false' }},
debug_phase: {{ debug_phase | tojson if debug_phase else 'null' }},
api_key: "test_api_key",
username: {{ username | tojson }},
ui_config: {}
};
// Conditionally enable/disable console logging based on ui_debug setting
if (!window.config.ui_debug) {
// Store original console methods
window._originalConsole = {
log: console.log,
debug: console.debug,
info: console.info,
warn: console.warn
};
// Override with no-ops (keep error for critical issues)
console.log = function() {};
console.debug = function() {};
console.info = function() {};
// Keep warn for important warnings
}
// Helper function to enable UI logging at runtime (for debugging in browser console)
window.enableUIDebug = function() {
if (window._originalConsole) {
console.log = window._originalConsole.log;
console.debug = window._originalConsole.debug;
console.info = window._originalConsole.info;
console.warn = window._originalConsole.warn;
console.log('UI debug logging enabled');
}
};
</script>
<script id="ui-config" type="application/json">{% if ui_config %}{{ ui_config | tojson | safe }}{% else %}{}{% endif %}</script>
<script>
(function(){
try {
var el = document.getElementById('ui-config');
if (el && el.textContent && el.textContent.trim().length > 0) {
window.config.ui_config = JSON.parse(el.textContent);
}
} catch (e) { console.warn('Failed to parse ui_config', e); }
})();
</script>
<!-- Load annotation JavaScript -->
<script src="{{ url_for('static', filename='interaction_tracker.js') }}?v=1"></script>
<script src="{{ url_for('static', filename='span-core.js') }}?v=11"></script>
<script src="{{ url_for('static', filename='ai_assistant_manager.js') }}?v=1"></script>
<!-- Display Logic Manager (conditional schema branching) - must load before annotation.js -->
<script src="{{ url_for('static', filename='display-logic.js') }}?v=1"></script>
<!-- Instance display manager (for new instance_display configuration) -->
{% if has_instance_display | default(false) %}
<script src="{{ url_for('static', filename='instance-display.js') }}?v=2"></script>
{% endif %}
<script src="{{ url_for('static', filename='annotation.js') }}?v=21"></script>
<!-- Fabric.js for image annotation (loaded from CDN) -->
{% if frontend_assets.image_annotation | default(false) %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/5.3.1/fabric.min.js"></script>
<!-- Image annotation manager -->
<script src="{{ url_for('static', filename='image-annotation.js') }}?v=2"></script>
{% endif %}
<!-- Visual AI Assistant for image/video annotation (loaded conditionally) -->
{% if ai_enabled and ((frontend_assets.image_annotation | default(false)) or (frontend_assets.video_annotation | default(false))) %}
<script src="{{ url_for('static', filename='visual_ai_assistant.js') }}?v=1"></script>
<!-- Option Highlight Manager for dimming less-likely options -->
<script src="{{ url_for('static', filename='option_highlight_manager.js') }}?v=2"></script>
{% endif %}
<!-- Peaks.js for audio/video annotation (stored locally for reliability) -->
{% if (frontend_assets.audio_annotation | default(false)) or (frontend_assets.video_annotation | default(false)) %}
<script src="{{ url_for('static', filename='peaks.min.js') }}"></script>
{% endif %}
<!-- Audio annotation manager -->
{% if frontend_assets.audio_annotation | default(false) %}
<script src="{{ url_for('static', filename='audio-annotation.js') }}?v=2"></script>
{% endif %}
<!-- Video annotation manager -->
{% if frontend_assets.video_annotation | default(false) %}
<script src="{{ url_for('static', filename='video-annotation.js') }}?v=10"></script>
{% endif %}
<!-- Span link manager -->
{% if frontend_assets.span_link | default(false) %}
<script src="{{ url_for('static', filename='span-link-manager.js') }}?v=7"></script>
{% endif %}
<!-- Event annotation manager -->
{% if frontend_assets.event_annotation | default(false) %}
<script src="{{ url_for('static', filename='event-annotation.js') }}?v=1"></script>
{% endif %}
<!-- Entity linking for knowledge bases (Wikidata, UMLS) -->
<script src="{{ url_for('static', filename='entity-linking.js') }}?v=1"></script>
<!-- Coreference chain manager -->
{% if frontend_assets.coreference | default(false) %}
<script src="{{ url_for('static', filename='coreference-manager.js') }}?v=1"></script>
{% endif %}
<!-- Segmentation tools -->
{% if frontend_assets.segmentation_tools | default(false) %}
<script src="{{ url_for('static', filename='segmentation-tools.js') }}?v=1"></script>
{% endif %}
<!-- Conversation tree interaction -->
{% if frontend_assets.conversation_tree | default(false) %}
<script src="{{ url_for('static', filename='conversation-tree.js') }}?v=1"></script>
{% endif %}
<!-- Video tracking interpolation and UI -->
{% if frontend_assets.tracking | default(false) %}
<script src="{{ url_for('static', filename='tracking-interpolation.js') }}?v=1"></script>
<script src="{{ url_for('static', filename='tracking-ui.js') }}?v=1"></script>
{% endif %}
<!-- Triage annotation manager -->
{% if frontend_assets.triage | default(false) %}
<script src="{{ url_for('static', filename='triage.js') }}?v=1"></script>
{% endif %}
<!-- Tiered annotation manager (ELAN-style hierarchical annotation) -->
{% if frontend_assets.tiered_annotation | default(false) %}
<script src="{{ url_for('static', filename='tiered-annotation.js') }}?v=1"></script>
{% endif %}
<!-- Document bounding box annotation -->
{% if frontend_assets.document_bbox | default(false) %}
<script src="{{ url_for('static', filename='document-bbox.js') }}?v=2"></script>
{% endif %}
<!-- PDF bounding box annotation -->
{% if frontend_assets.pdf_bbox | default(false) %}
<script src="{{ url_for('static', filename='pdf-bbox.js') }}?v=2"></script>
{% endif %}
{% if agent_proxy_enabled | default(false) %}
<!-- Agent Chat UI -->
<script src="{{ url_for('static', filename='agent-chat.js') }}?v=1"></script>
{% endif %}
{% if chat_enabled | default(false) %}
<!-- LLM Chat Sidebar -->
<script src="{{ url_for('static', filename='llm-chat-sidebar.js') }}?v=1"></script>
{% endif %}
<!-- Web Agent Trace Viewer (overlays + step navigation) -->
{% if frontend_assets.web_agent_viewer | default(false) %}
<script src="{{ url_for('static', filename='web-agent-overlays.js') }}?v=1"></script>
<script src="{{ url_for('static', filename='web-agent-viewer.js') }}?v=1"></script>
{% endif %}
{% if frontend_assets.web_agent_playback | default(false) %}
<script src="{{ url_for('static', filename='web-agent-playback.js') }}?v=1"></script>
{% endif %}
<!-- Web Agent Interaction Recorder (creation mode) -->
{% if frontend_assets.web_agent_recorder | default(false) %}
<script src="{{ url_for('static', filename='web-agent-recorder.js') }}?v=1"></script>
{% endif %}
<!-- Live Agent Viewer (real-time agent interaction) -->
{% if live_agent_enabled | default(false) %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/live-agent.css') }}?v=1">
<script src="{{ url_for('static', filename='live-agent-viewer.js') }}?v=1"></script>
{% endif %}
<!-- Global keyboard debug (temporary) -->
<script>
window.addEventListener('keydown', function(e) {
console.log('[GLOBAL DEBUG] keydown:', e.key, 'code:', e.code, 'target:', e.target.tagName);
}, true);
</script>
<!-- Instance height limit functionality -->
<script>
document.addEventListener('DOMContentLoaded', function() {
// Apply maximum instance height if configured
if (window.config && window.config.ui_config && window.config.ui_config.max_instance_height) {
const maxHeight = window.config.ui_config.max_instance_height;
const instanceTextContainer = document.getElementById('instance-text');
if (instanceTextContainer) {
// Set CSS custom property for max height
instanceTextContainer.style.setProperty('--max-instance-height', maxHeight + 'px');
// Add the max-height-limited class to enable scrolling
instanceTextContainer.classList.add('max-height-limited');
console.log('Applied maximum instance height:', maxHeight + 'px');
}
}
});
</script>
<!-- Universal Memos sidebar (self-hides if annotation_ui.memos is off:
the API returns 503 and the toggle stays hidden) -->
{% if is_annotation_page %}
<button type="button" id="memo-panel-toggle" class="memo-panel-toggle" hidden>
&#128221; Notes
</button>
<div id="memo-panel" class="memo-panel" hidden aria-label="Notes panel">
<div class="memo-panel-header">
<span>Notes</span>
<button type="button" id="memo-panel-close" aria-label="Close notes">&times;</button>
</div>
<div id="memo-list" class="memo-list"></div>
<div class="memo-composer">
<textarea id="memo-new-body" placeholder="Add a note about this instance&hellip;"></textarea>
<div id="memo-anchor-wrap" class="memo-anchor-hint" hidden>
<label>
<input type="checkbox" id="memo-anchor-check">
Attach to selected text: &ldquo;<span id="memo-anchor-quote"></span>&rdquo;
</label>
</div>
<div class="memo-composer-row">
<label>Visibility
<select id="memo-new-visibility">
<option value="private">Private</option>
<option value="shared">Shared</option>
</select>
</label>
<button type="button" id="memo-add-btn" class="memo-primary">Add note</button>
</div>
</div>
</div>
<script src="{{ url_for('static', filename='memos.js') }}?v=1"></script>
<!-- Annotator search-and-claim sidebar (self-hides unless
search.annotator_claim is enabled: the enable-probe only gets
400 when the feature is on and the user is authed). -->
<button type="button" id="search-panel-toggle" class="search-panel-toggle" hidden>
&#128269; Find
</button>
<div id="search-panel" class="search-panel" hidden aria-label="Search panel">
<div class="search-panel-header">
<span>Find instances</span>
<button type="button" id="search-panel-close" aria-label="Close search">&times;</button>
</div>
<div class="search-form">
<input type="text" id="search-q" placeholder="Search instance text&hellip;"
aria-label="Search query">
<button type="button" id="search-go" class="search-primary">Search</button>
</div>
<div id="search-results" class="search-results" aria-live="polite">
<div class="search-empty">Search to find instances, then Claim
one to add it to your queue.</div>
</div>
</div>
<script src="{{ url_for('static', filename='search.js') }}?v=2"></script>
<!-- Universal Codebook tray (self-hides unless the codebook is
enabled: the enable-probe is not 200 otherwise). The composer
only appears when codebook_mode is extensible/open. -->
<button type="button" id="cb-panel-toggle" class="cb-panel-toggle" hidden>
&#127991; Codebook
</button>
<div id="cb-panel" class="cb-panel" hidden role="region"
aria-label="Codebook panel">
<div class="cb-panel-header">
<span>Codebook</span>
<button type="button" id="cb-panel-close"
aria-label="Close codebook">&times;</button>
</div>
<div id="cb-stale-banner" class="cb-stale-banner" hidden>
<span class="cb-stale-msg" id="cb-stale-msg"
role="status"></span>
<button type="button" id="cb-stale-dismiss"
class="cb-stale-x"
aria-label="Dismiss this notice">&times;</button>
</div>
<div id="cb-tree" class="cb-tree" aria-live="polite">
<div class="cb-empty">Loading&hellip;</div>
</div>
<div class="cb-worklist-section" id="cb-worklist-section" hidden>
<h2 id="cb-worklist-head" class="cb-worklist-head">Review</h2>
<div id="cb-worklist" class="cb-worklist"
aria-labelledby="cb-worklist-head">
<div class="cb-empty">Nothing to review.</div>
</div>
</div>
<div id="cb-admin-section" class="cb-admin-section" hidden>
<h2 class="cb-admin-head">Curate (admin)</h2>
<div class="cb-admin-group">
<label class="cb-admin-label"
for="cb-merge-src">Merge a code into another</label>
<div class="cb-admin-row">
<select id="cb-merge-src" class="cb-admin-select"
aria-label="Code to merge from"></select>
<span class="cb-admin-arrow" aria-hidden="true"></span>
<select id="cb-merge-dst" class="cb-admin-select"
aria-label="Code to merge into"></select>
</div>
<button type="button" id="cb-merge-btn"
class="cb-primary">Merge</button>
</div>
<div class="cb-admin-group">
<label class="cb-admin-label"
for="cb-split-src">Split a code by annotator</label>
<div class="cb-admin-row">
<select id="cb-split-src" class="cb-admin-select"
aria-label="Code to split"></select>
<input type="text" id="cb-split-annotator"
class="cb-admin-input"
placeholder="annotator"
aria-label="Annotator to split out">
</div>
<input type="text" id="cb-split-name"
class="cb-admin-input"
placeholder="New code name&hellip;"
aria-label="New code name">
<button type="button" id="cb-split-btn"
class="cb-primary">Split</button>
</div>
<div id="cb-admin-error" class="cb-error" hidden
role="alert" tabindex="-1"></div>
<div id="cb-admin-status" class="cb-admin-status"
role="status" aria-live="polite"></div>
<div class="cb-admin-group">
<h3 class="cb-admin-sub" id="cb-proposals-head">
Pending proposals</h3>
<div id="cb-proposals" class="cb-proposals"
aria-labelledby="cb-proposals-head"
aria-live="polite">
<div class="cb-empty">No pending proposals.</div>
</div>
</div>
<details class="cb-admin-group cb-changes-wrap">
<summary class="cb-admin-sub">Recent changes</summary>
<ul id="cb-changes" class="cb-changes-list"></ul>
</details>
</div>
<div id="cb-composer" class="cb-composer" hidden>
<div class="cb-composer-row">
<input type="text" id="cb-new-name"
placeholder="Add a code&hellip;"
aria-label="New code name">
<button type="button" id="cb-add-btn"
class="cb-primary">Add</button>
</div>
<div id="cb-error" class="cb-error" hidden></div>
<div id="cb-mode-hint" class="cb-mode-hint"></div>
</div>
</div>
<script src="{{ url_for('static', filename='codebook.js') }}?v=10"></script>
{% endif %}
</body>
</html>