|
|
| import React, { useState } from "react"; |
| import ReactPlayer from "react-player"; |
| import axios from "axios"; |
| import AxisEvaluation from "./components/AxisEvaluation"; |
| import initialVideos from "./videoData"; |
|
|
| const API_BASE_URL = ( |
| import.meta.env.VITE_API_BASE_URL || |
| (import.meta.env.DEV ? "http://localhost:8000" : "") |
| ).replace(/\/$/, ""); |
|
|
| |
| |
| const EXAMPLE_VIDEO_INDEX = 0; |
|
|
| const evaluationAxes = [ |
| { |
| id: "audio_content", |
| title: "Audio Content", |
| summary: "Evaluate each required sound or speech item independently.", |
| details: [ |
| "Label every listed audio item separately, such as a slap sound, clap sound, spoken word, silence, or absence of background music.", |
| "Mark No for a specific item if that sound is missing, replaced by the wrong sound, or contradicted by extra audio.", |
| "Do not judge visual correctness here unless it affects what can be heard." |
| ] |
| }, |
| { |
| id: "visual_content", |
| title: "Visual Content", |
| summary: "Evaluate each required visible object, gesture, person, or action independently.", |
| details: [ |
| "Label every listed visual item separately, including which hand/body part is used, what gesture/action occurs, and what scene elements are present.", |
| "Mark No for a specific item if it is missing, replaced, unclear, or performed by the wrong person/body part.", |
| "Do not judge sound or timing here unless it affects what can be seen." |
| ] |
| }, |
| { |
| id: "audio_visual_sync", |
| title: "Audio-Visual Synchronization", |
| summary: "Evaluate whether each audio event is synchronized with its corresponding visual event.", |
| details: [ |
| "Only evaluate an audio-visual pair when the corresponding audio item and visual item are both correct enough to compare.", |
| "Mark No if the sound clearly leads or lags the visible event, or if a sound has no matching visual cause.", |
| "Use slower playback when synchronization is hard to judge." |
| ] |
| }, |
| { |
| id: "temporal_relation", |
| title: "Temporal Relation", |
| summary: "Evaluate requested ordering or temporal relationships separately from counts.", |
| details: [ |
| "Only evaluate temporal relations when the relevant Audio Content, Visual Content, and Audio-Visual Synchronization components are correct enough to support the judgment.", |
| "Check requested relations such as before/after order, simultaneous events, synchronization as a temporal relation, pauses, or ending state.", |
| "Do not use this axis to judge how many times an event occurs; use Temporal Count for that." |
| ] |
| }, |
| { |
| id: "temporal_count", |
| title: "Temporal Count", |
| summary: "Enter how many times each requested event actually occurs in the video.", |
| details: [ |
| "Only count an event when the relevant Audio Content, Visual Content, Audio-Visual Synchronization, and Temporal Relation components are correct enough to identify it.", |
| "Count each occurrence of the listed event and type the total number you observe.", |
| "Enter 0 if the event does not occur at all." |
| ] |
| }, |
| { |
| id: "constraint_following", |
| title: "Constraint Following", |
| summary: "Check the non-core prompt constraints as a single overall judgment.", |
| details: [ |
| "Check scene, setting, camera, shot type, people count, subtitles or on-screen text, background music, extra sounds, extra speech, extra gestures or actions, and stillness after the required events.", |
| "Mark No if the clip violates any of these constraints stated in the prompt.", |
| "Use this as a final overall check after judging the more specific axes above." |
| ] |
| } |
| ]; |
|
|
| const createEmptyAnnotations = (video) => |
| Object.fromEntries( |
| evaluationAxes.map((axis) => [ |
| axis.id, |
| { |
| items: Object.fromEntries( |
| (video.evaluationItems[axis.id] || []).map((item) => [ |
| item.id, |
| { label: "", rationale: "" } |
| ]) |
| ) |
| } |
| ]) |
| ); |
|
|
| export default function App() { |
| const [hasStarted, setHasStarted] = useState(false); |
| const [understandsInstructions, setUnderstandsInstructions] = useState(false); |
| const [showExample, setShowExample] = useState(false); |
| const [exampleCompleted, setExampleCompleted] = useState(false); |
| const [playbackRate, setPlaybackRate] = useState(1); |
|
|
| const [videos, setVideos] = useState(initialVideos); |
| const [currentIndex, setCurrentIndex] = useState(0); |
| const currentVideo = videos[currentIndex]; |
|
|
| const [annotator, setAnnotator] = useState(""); |
|
|
| const [annotations, setAnnotations] = useState(() => createEmptyAnnotations(currentVideo)); |
|
|
| const updateAxisItem = (axisId, itemId, field, value) => { |
| setAnnotations((prev) => ({ |
| ...prev, |
| [axisId]: { |
| ...prev[axisId], |
| items: { |
| ...prev[axisId].items, |
| [itemId]: { |
| ...prev[axisId].items[itemId], |
| [field]: value |
| } |
| } |
| } |
| })); |
| }; |
|
|
| const saveAnnotations = async () => { |
| const payload = { |
| video_id: currentVideo.video_id, |
| annotations: { |
| responses: annotations, |
| axis_definitions: evaluationAxes.map(({ id, title, summary }) => ({ |
| id, |
| title, |
| summary |
| })), |
| evaluation_items: currentVideo.evaluationItems, |
| temporal_constraints: currentVideo.constraints, |
| video_metadata: currentVideo.metadata, |
| source_details: currentVideo.source_details |
| }, |
| user: annotator || "anonymous" |
| }; |
|
|
| try { |
| await axios.post(`${API_BASE_URL}/save_annotation`, payload); |
|
|
| alert("Annotations saved."); |
|
|
| |
| if (currentIndex < videos.length - 1) { |
| setCurrentIndex((i) => i + 1); |
| |
| setAnnotations(createEmptyAnnotations(videos[currentIndex + 1])); |
| } else { |
| alert("All videos completed."); |
| } |
| } catch (err) { |
| console.error(err); |
| const status = err.response?.status ? `Status: ${err.response.status}. ` : ""; |
| const detail = err.response?.data?.detail || err.message || "Unknown error"; |
| alert(`Error saving annotations.\n${status}${detail}\nBackend: ${API_BASE_URL}`); |
| } |
| }; |
|
|
| if (!hasStarted) { |
| return ( |
| <main className="instructions-page"> |
| <section className="instructions-panel"> |
| <div className="instructions-header"> |
| <p className="eyebrow">Annotation task</p> |
| <h1>T2AV Video Evaluation</h1> |
| <p> |
| The goal of this evaluation is to assess whether a |
| Text-to-Audio-Video generation model correctly follows the given |
| prompt by producing the required sounds, visual actions, and |
| temporal constraints. Your annotations should reflect only what is |
| actually visible and audible in the video, not what the prompt says |
| should happen. |
| </p> |
| </div> |
| |
| <div className="instructions-grid"> |
| <section> |
| <h2>Event Types</h2> |
| <ul> |
| <li><strong>Speech:</strong> spoken words, such as "okay", "help", or "no".</li> |
| <li><strong>Silent action:</strong> a soundless gesture, such as a thumbs-up, head nod, or hand wave.</li> |
| <li><strong>Sounding action:</strong> an action that produces a sound, such as a clap, stomp, or thigh slap.</li> |
| </ul> |
| </section> |
| |
| <section> |
| <h2>What You'll See for Each Video</h2> |
| <ul> |
| <li>The generation prompt for the video.</li> |
| <li>A target event checklist of the required speech, action, and sound events.</li> |
| <li>The temporal constraints, such as count, order, or synchronization.</li> |
| <li>A short description of each target action or sound.</li> |
| </ul> |
| </section> |
| |
| <section> |
| <h2>How to Evaluate</h2> |
| <ul> |
| <li>Watch the full video before selecting labels.</li> |
| <li>Use slower playback when timing or synchronization is unclear.</li> |
| <li>Base every decision only on the provided video and audio.</li> |
| <li>Each axis already lists only the items relevant to that video, so judge exactly the items shown.</li> |
| </ul> |
| </section> |
| |
| <section> |
| <h2>Labels</h2> |
| <ul> |
| <li><strong>Yes:</strong> the specific listed item is clearly satisfied.</li> |
| <li><strong>No:</strong> the specific listed item is clearly violated or missing. Treat ambiguous or hard-to-judge cases as No.</li> |
| <li>Temporal Count is not Yes/No: type the number of times the event actually occurs.</li> |
| </ul> |
| </section> |
| |
| <section> |
| <h2>Labeling Order</h2> |
| <ul> |
| <li>Audio Content first: is the required speech or sound correct?</li> |
| <li>Then Visual Content: is the required visual action correct?</li> |
| <li>Then Audio-Visual Synchronization, only when the matching audio and visual items are correct enough to compare.</li> |
| <li>Then Temporal Relation and Temporal Count, only when the relevant audio, visual, and synchronization items are correct enough to identify the event.</li> |
| <li>Finally Constraint Following: are the non-core prompt constraints satisfied?</li> |
| </ul> |
| </section> |
| |
| {evaluationAxes.map((axis) => ( |
| <section key={axis.id}> |
| <h2>{axis.title}</h2> |
| <p className="axis-summary">{axis.summary}</p> |
| <ul> |
| {axis.details.map((detail) => ( |
| <li key={detail}>{detail}</li> |
| ))} |
| </ul> |
| </section> |
| ))} |
| |
| <section> |
| <h2>Temporal Constraint Types</h2> |
| <ul> |
| <li><strong>Count:</strong> the target behavior must occur exactly N times. Count only clear, completed occurrences.</li> |
| <li><strong>Order:</strong> the required events must appear in the exact stated order, without reversal or unintended overlap.</li> |
| <li><strong>Synchronization:</strong> the required events must occur simultaneously or with tight temporal alignment.</li> |
| <li><strong>Rhythm:</strong> the behavior must repeat with the required rhythm.</li> |
| <li><strong>Spacing:</strong> the gaps between events must match the spacing required in the prompt.</li> |
| <li><strong>Absolute timing:</strong> onsets, pauses, and durations must match the exact times in the prompt.</li> |
| <li><strong>Start/stop:</strong> the behavior must start and stop according to the trigger in the prompt.</li> |
| </ul> |
| </section> |
| |
| <section> |
| <h2>Important Notes</h2> |
| <ul> |
| <li>Each axis lists only the items required by the prompt, so you never need to mark an item as "not applicable".</li> |
| <li>If the prompt specifies the right hand but the left hand is used, mark that visual item No. Other axes can still be Yes if otherwise satisfied.</li> |
| <li>If a required event does not appear at all, mark every item that depends on it No.</li> |
| <li>When an item is genuinely ambiguous due to video quality, occlusion, or camera angle, mark it No.</li> |
| </ul> |
| </section> |
| |
| <section> |
| <h2>Rationales</h2> |
| <ul> |
| <li>Write a short reason (1-2 sentences) for every label.</li> |
| <li>Describe only observable evidence from the video; do not infer the model's intention.</li> |
| <li>Mention the specific sound, gesture, count, order, or timing you observed.</li> |
| <li>Do not leave rationale fields blank.</li> |
| </ul> |
| <p className="rationale-examples">Examples: "The word 'help' is clearly audible once." or "The person raises a hand, but it is not a clear right-hand thumbs-up."</p> |
| </section> |
| |
| <section> |
| <h2>Example/Test Case</h2> |
| <p>Watch the example video to calibrate your annotations before starting.</p> |
| <div className="example-controls"> |
| <button onClick={() => setShowExample((s) => !s)}> |
| {showExample ? "Hide Example" : "Play Example"} |
| </button> |
| <label className="example-done"> |
| <input |
| type="checkbox" |
| checked={exampleCompleted} |
| onChange={(e) => setExampleCompleted(e.target.checked)} |
| /> |
| I have watched the example video |
| </label> |
| </div> |
| {showExample && ( |
| <div className="example-player"> |
| <ReactPlayer |
| url={videos[EXAMPLE_VIDEO_INDEX].video_url} |
| controls |
| width="100%" |
| /> |
| </div> |
| )} |
| </section> |
| </div> |
| |
| <div className="start-panel"> |
| <label className="name-field"> |
| Annotator name |
| <input |
| value={annotator} |
| onChange={(e) => setAnnotator(e.target.value)} |
| placeholder="Your name" |
| /> |
| </label> |
| |
| <label className="confirmation-row"> |
| <input |
| type="checkbox" |
| checked={understandsInstructions} |
| onChange={(e) => setUnderstandsInstructions(e.target.checked)} |
| /> |
| <span>I have read and understand the annotation instructions.</span> |
| </label> |
| |
| <button |
| className="save-btn start-btn" |
| disabled={!understandsInstructions || !annotator.trim() || !exampleCompleted} |
| onClick={() => setHasStarted(true)} |
| > |
| Start Annotation |
| </button> |
| </div> |
| </section> |
| </main> |
| ); |
| } |
|
|
| return ( |
| <div className="container"> |
| |
| <div className="card video-card"> |
| <h2>Video Evaluation</h2> |
| |
| <ReactPlayer |
| key={currentVideo.video_id} |
| url={currentVideo.video_url} |
| controls |
| width="100%" |
| playbackRate={playbackRate} |
| /> |
| |
| <div className="controls"> |
| <button onClick={() => setPlaybackRate(0.25)}> |
| 0.25x |
| </button> |
| |
| <button onClick={() => setPlaybackRate(0.5)}> |
| 0.5x |
| </button> |
| |
| <button onClick={() => setPlaybackRate(1)}> |
| 1x |
| </button> |
| </div> |
| </div> |
| |
| <div className="card annotation-card"> |
| <h2>Prompt</h2> |
| <div className="metadata-row"> |
| <span>Item: {currentVideo.item_id}</span> |
| <span>Version: {currentVideo.variant}</span> |
| </div> |
| <p>{currentVideo.prompt}</p> |
| |
| <div className="annotator-display">Annotator: {annotator}</div> |
| |
| <h3>Target Events</h3> |
| <div className="target-events"> |
| {[ |
| ...(currentVideo.evaluationItems?.visual_content || []), |
| ...(currentVideo.evaluationItems?.audio_content || []) |
| ].map((it) => ( |
| <span className="tag" key={it.id}> |
| {it.title} |
| </span> |
| ))} |
| </div> |
| |
| <h3>Temporal Constraints</h3> |
| {currentVideo.constraints.map((constraint) => ( |
| <div className="tag" key={`${constraint.type}-${constraint.text}`}> |
| <strong>{constraint.type}:</strong> {constraint.text} |
| </div> |
| ))} |
| |
| <hr /> |
| |
| {evaluationAxes.map((axis) => ( |
| <AxisEvaluation |
| key={axis.id} |
| axis={axis} |
| data={annotations[axis.id]} |
| items={currentVideo.evaluationItems[axis.id] || []} |
| updateAxisItem={updateAxisItem} |
| /> |
| ))} |
| |
| <button |
| className="save-btn" |
| onClick={saveAnnotations} |
| > |
| Save Annotation |
| </button> |
| </div> |
| </div> |
| ); |
| } |
|
|