Code viewer for World: AI Song Generator
// Cloned by Niall Ryan on 24 Nov 2023 from World "Calling APIs" by Niall Ryan 
// Please leave this clone trail here.

// Cloned by Niall Ryan on 22 Nov 2023 from World "Chat with GPT model" by Starter user 
// Please leave this clone trail here.

// Sample prompt for a song
let prompt = "";
let key = "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";

// METHOD FOR CALLING AI APIs
async function fetchData(apiUrl, data) {
    
    // MH edit
    console.log ( "fetchData: " + apiUrl );
    console.log ( data );

  const response = await fetch(apiUrl, {
    headers: {
      Authorization: `Bearer ${key}`
    },
    method: "POST",
    body: JSON.stringify(data),
  });

  if (!response.ok) {
    throw new Error(`HTTP Error! status: ${response.status}`);
  }
  return response;
}

// Function handles the query to the image generating AI API
async function queryImage(data) {
  // Add a 'loader' to the image container whilst awaiting the API
  let loader = document.createElement('div');
  loader.className = 'loader';
  document.getElementById('image').appendChild(loader);
 
  try {
    const response = await fetchData("https://api-inference.huggingface.co/models/runwayml/stable-diffusion-v1-5", data);
    const imageBlob = await response.blob();
    updateUI('image', imageBlob, true);
  } catch (error) {
    handleError('image', error.message);
  }
}

// General function for querying sound API (small & medium sized models)
async function querySound(data, apiUrl, containerId) {
  // Add a 'loader' to the image container whilst awaiting the API
  let loader = document.createElement('div');
  loader.className = 'loader';
  document.getElementById(containerId).appendChild(loader);

  try {
    const response = await fetchData(apiUrl, data);
    if (response.headers.get("content-type") === "audio/flac") {
      const audioBlob = await response.blob();
      updateUI(containerId, audioBlob, false);
    } else {
      const result = await response.json();
      return result;
    }
  } catch (error) {
    handleError(containerId, error.message);
  }
}


// Function updates the UI with new content or error messages
function updateUI(containerId, content, isImage) {
  const container = document.getElementById(containerId);
  container.innerHTML = ''; // Clear previous content or loaders

  if (isImage) {
    const image = document.createElement('img');
    image.src = URL.createObjectURL(content);
    container.appendChild(image);
  } else {
    const audio = new Audio(URL.createObjectURL(content));
    audio.controls = true;
    container.appendChild(audio);
    audio.addEventListener('canplaythrough', () => audio.play());
  }
}

function clearUI() {
    document.getElementById('image').innerHTML = '';
    document.getElementById('audioSmall').innerHTML = '';
    document.getElementById('audioMedium').innerHTML = '';
}

// Error handler for UI updates
function handleError(containerId, status) {
  const container = document.getElementById(containerId);
  container.innerHTML = ''; // Clear loaders

  const errorElement = document.createElement('p');
  errorElement.className = 'errorText';
  errorElement.textContent = `ERROR: ${containerId} returned ${status}`;
  container.appendChild(errorElement);

  console.error(`HTTP Error! status: ${status}`);
}

// Function that handles the user's custom prompt input
async function handleUserInput() {
  // Clear any existing inference results
  clearUI()

  // Get the user prompt
  const userInput = document.getElementById('userPrompt').value;
  const prompt = userInput; // Use the user input as the new prompt

  // Update the displayed prompt
  document.getElementById('prompt').innerHTML = `<b>Prompt:</b> ${prompt}`;

  // Execute the API calls with the new prompt
  const imagePromise = queryImage({ inputs: `Album cover art for a song described as "${prompt}"` });
  const soundSmallPromise = querySound({ inputs: prompt }, "https://api-inference.huggingface.co/models/facebook/musicgen-small", 'audioSmall');
  const soundMediumPromise = querySound({ inputs: prompt }, "https://api-inference.huggingface.co/models/facebook/musicgen-medium", 'audioMedium');
  
  // Execute all the API calls at once
  await Promise.all([imagePromise, soundSmallPromise, soundMediumPromise]);
}

// The actual document HTML
document.write(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI Song Generator</title>
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap" rel="stylesheet"/>
<style>
  body {
    font-family: 'Roboto', sans-serif;
    text-align: center;
    margin: 0;
    color: #fff;
  }
  .background {
      width: 100%;
      height: 100%;
      position: fixed;
      top: 0;
      left: 0;
      background-color: #333;
  }
  .container {
    background-color: #282828;
    border-radius: 10px;
    box-shadow: 0 4px 8px rgba(4, 170, 109,0.2);
    margin: auto;
    padding: 20px;
    max-width: 800px;
    margin-top: 40px;
  }
  img, audio {
    max-width: 100%;
    border-radius: 5px;
    margin-top: 20px;
  }
  .input-field {
    width: 80%;
    padding: 12px 20px;
    margin: 8px 0;
    display: inline-block;
    border: 3px solid #04AA6D;
    border-radius: 6px;
    box-sizing: border-box;
    background-color: #f0f0f0;
    color: #333;
    font-size: 16px;
    transition: border 0.3s;
  }
  .input-field:focus {
    border-color: #026D4D;
    outline: none;
  }
  .input-field::placeholder {
    color: #7a7a7a;
  }
  .input-field:hover {
    border-color: #013D2D;
  }
  .generate-button {
    padding: 10px 20px;
    border-radius: 5px;
    border: none;
    background-color: #04AA6D;
    color: #D6EEE2;
    color: white;
    font-size: 16px;
    cursor: pointer;
    transition: background-color 0.3s ease;
  }
  .generate-button:hover {
    background-color: #D6EEE2;
    color: #04AA6D;
  }
  .loader {
    border: 5px solid #D6EEE2;
    border-top: 5px solid #04AA6D;
    border-radius: 50%;
    width: 50px;
    height: 50px;
    animation: spin 2s linear infinite;
    margin: 20px auto;
  }
  @keyframes spin {
    0% { transform: rotate(0deg); }
    100% { transform: rotate(360deg); }
  }
  .errorText {
      color: #e74c3c;
      padding: 10px;
      font-weight: 900;
  }
  .prompt-display {
    background-color: #333;
    border-left: 5px solid #35634C;
    padding: 10px;
    margin: 20px 0;
    display: block;
    text-align: left;
  }
  .prompt-display b {
      color: #D6EEE2
  }
  .title {
    color: #04AA6D;
    margin-bottom: 5px;
    margin: 0 0 20px 0;
  }
  footer {
      position: fixed;
      left: 0;
      bottom: 0;
      width: 100%;
      background-color: #282828;
      padding: 10px 0;
  }
  .footer-content {
      display: flex;
      justify-content: center;
      align-items: center;
  }
  .green-bar {
      color: 00ff00;
      font-weight: 900;
      margin: 0 10px 0 20px;
  }
</style>
</head>
<body>
<div class="background">
<div class="container">
  <h1 class="title">AI Song Generator</h1>
  <input type="text" id="userPrompt" class="input-field" placeholder="Describe your fictional song..." value="${prompt}">
  <button onclick="handleUserInput()" class="generate-button">Generate</button>
  <span class="prompt-display" id="prompt"><b>Prompt:</b> ${prompt}</span>
  <div id="image"></div>
  <div id="audioSmall" class="pad"></div>
  <div id="audioMedium" class="pad"></div>
</div>
</div>
<footer>
  <div class="footer-content">
    <span class="green-bar">|</span>
    Audio APIs are unreliable due to their popularity. 
    <span class="green-bar">|</span>
    Audio APIs take ~30s to perform inference and return info.
  </div>
</footer>
</body>
</html>
`);