Merge pull request #278 from storytold/olivicmic-fy-lipsync-revamp

Interface for Face Animation
This commit is contained in:
Victor O authored and GitHub committed 2023-09-27 16:56:30 -07:00
commit 8a0330af45
70 files changed
+2441 -190

No files matched your search

+1
View File
@@ -33,6 +33,7 @@
"lerna": "^4.0.0"
},
"dependencies": {
"@react-spring/types": "^9.7.3",
"yarn": "^1.22.19"
},
"packageManager": "yarn@3.3.1"
+4
View File
@@ -420,6 +420,10 @@ class ApiConfig {
return `${this.getApiOrigin()}/v1/media_uploads/upload_image`;
}
uploadVideo(): string {
return `${this.getApiOrigin()}/v1/media_uploads/upload_video`;
}
uploadMedia(): string {
return `${this.getApiOrigin()}/v1/media_uploads/upload`;
}
@@ -20,24 +20,36 @@ export interface EnqueueFaceAnimationRequest {
image_source: ImageSource,
}
export interface EnqueueFaceAnimationSuccessResponse {
// export interface EnqueueFaceAnimationSuccessResponse {
// success: boolean,
// inference_job_token: string,
// }
// export interface EnqueueFaceAnimationErrorResponse {
// success: boolean,
// }
export interface EnqueueFaceAnimationResponse {
success: boolean,
inference_job_token: string,
inference_job_token?: string,
}
export interface EnqueueFaceAnimationErrorResponse {
success: boolean,
}
type EnqueueFaceAnimationResponse = EnqueueFaceAnimationSuccessResponse | EnqueueFaceAnimationErrorResponse;
const EnqueueFaceAnimationIsSuccess = (response: EnqueueFaceAnimationResponse) => response.inference_job_token;
const EnqueueFaceAnimationIsError = (response: EnqueueFaceAnimationResponse) => !response.inference_job_token;
export function EnqueueFaceAnimationIsSuccess(response: EnqueueFaceAnimationResponse): response is EnqueueFaceAnimationSuccessResponse {
return response?.success === true;
}
export { EnqueueFaceAnimationIsSuccess, EnqueueFaceAnimationIsError };
// export EnqueueFaceAnimationIsSuccess;
// export EnqueueFaceAnimationIsError;
export function EnqueueFaceAnimationIsError(response: EnqueueFaceAnimationResponse): response is EnqueueFaceAnimationErrorResponse {
return response?.success === false;
}
// export function EnqueueFaceAnimationIsSuccess(response: EnqueueFaceAnimationResponse): response is EnqueueFaceAnimationSuccessResponse {
// return response?.success === true;
// }
// export function EnqueueFaceAnimationIsError(response: EnqueueFaceAnimationResponse): response is EnqueueFaceAnimationErrorResponse {
// return response?.success === false;
// }
export async function EnqueueFaceAnimation(request: EnqueueFaceAnimationRequest) : Promise<EnqueueFaceAnimationResponse>
{
@@ -0,0 +1,54 @@
import { ApiConfig } from "../ApiConfig";
export interface UploadRequest {
uuid_idempotency_token: string,
source?: string, // eg. "device", "file"
type: string, // "audio", "image", "video"
file: any,
};
export interface UploadResponse {
success: boolean,
upload_token?: string,
}
const { uploadAudio, uploadImage, uploadVideo } = new ApiConfig();
const audio = new ApiConfig().uploadAudio();
const image = new ApiConfig().uploadImage();
const video = new ApiConfig().uploadVideo();
const endpoints = { audio, image, video };
export async function Upload(request: UploadRequest) : Promise<UploadResponse>
{
const formData = new FormData();
formData.append('uuid_idempotency_token', request.uuid_idempotency_token);
formData.append('file', request.file);
if (request.source !== undefined) {
formData.append('source', request.source);
}
return fetch(endpoints[request.type as keyof typeof endpoints], {
method: 'POST',
credentials: 'include',
headers: {
'Accept': 'application/json',
},
body: formData,
})
.then(res => res.json())
.then(res => {
if (res && 'success' in res) {
return res;
} else {
return { success : false };
}
})
.catch(e => {
return { success : false };
});
}
+1
View File
@@ -15,6 +15,7 @@
"@fortawesome/pro-regular-svg-icons": "^6.2.1",
"@fortawesome/pro-solid-svg-icons": "^6.2.1",
"@fortawesome/react-fontawesome": "^0.2.0",
"@react-spring/web": "^9.7.3",
"@storyteller/components": "0.1.0",
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.5.0",
@@ -0,0 +1,24 @@
{
"headings": {
"audio": "শ্রুতি",
"image": "ছবি",
"subtitle": "একটি পরিষ্কার মুখ এবং একটি অডিও নমুনা সহ নির্বাচন করুন এবং চিত্র করুন এবং একটি লিপসিন্সড ভিডিও তৈরি করুন৷",
"title": "ফেস অ্যানিমেটর"
},
"inputs": {
"downloadFile": "ডাউনলোড ফাইল",
"generate": "তৈরি করুন",
"makeAnother": "আরেকটা তৈরি কর"
},
"status": {
"animationComplete": "অ্যানিমেশন সম্পূর্ণ!",
"animationDead": "অ্যানিমেশন অনুরোধ মৃত",
"animationFailed": "অ্যানিমেশন ব্যর্থ হয়েছে {{attemptCount}} প্রচেষ্টা(গুলি) আবার চেষ্টা করবে...",
"animationInProgress": "অ্যানিমেশন চলছে",
"animationPending": "অ্যানিমেশন মুলতুবি...",
"animationSuccess": "অ্যানিমেশন সফল",
"requestingAnimation": "অ্যানিমেশন অনুরোধ",
"uploadingAudio": "অডিও আপলোড করা হচ্ছে",
"uploadingImage": "ছবি আপলোড করা হচ্ছে"
}
}
@@ -0,0 +1,24 @@
{
"headings": {
"audio": "Audio",
"image": "Bild",
"subtitle": "Wählen Sie ein Bild mit einem klaren Gesicht und einem Audiobeispiel aus und erstellen Sie ein lippensynchrones Video.",
"title": "Gesichtsanimator"
},
"inputs": {
"downloadFile": "Download-Datei",
"generate": "Generieren",
"makeAnother": "Mach etwas anderes"
},
"status": {
"animationComplete": "Animation abgeschlossen!",
"animationDead": "Animationsanfrage tot",
"animationFailed": "Animation fehlgeschlagen {{attemptCount}} Versuche). Werde es noch einmal versuchen...",
"animationInProgress": "Animation läuft",
"animationPending": "Animation ausstehend...",
"animationSuccess": "Animation gelungen",
"requestingAnimation": "Animation anfordern",
"uploadingAudio": "Audio hochladen",
"uploadingImage": "Bild hochladen"
}
}
@@ -0,0 +1,24 @@
{
"headings": {
"audio": "Audio",
"image": "Image",
"subtitle": "Select and image with a clear face, and an audio sample, and generate a lipsynced video.",
"title": "Face Animator"
},
"inputs": {
"downloadFile": "Download File",
"generate": "Generate",
"makeAnother": "Make another"
},
"status": {
"animationComplete": "Animation complete!",
"animationDead": "Animation request dead",
"animationFailed": "Animation failed {{attemptCount}} attempt(s). Will retry...",
"animationInProgress": "Animation in progress",
"animationPending": "Animation pending...",
"animationSuccess": "Animation successful",
"requestingAnimation": "Requesting Animation",
"uploadingAudio": "Uploading Audio",
"uploadingImage": "Uploading Image"
}
}
@@ -0,0 +1,24 @@
{
"headings": {
"audio": "Audio",
"image": "Imagen",
"subtitle": "Seleccione una imagen con una cara clara y una muestra de audio, y genere un video sincronizado con los labios.",
"title": "Animador de rostros"
},
"inputs": {
"downloadFile": "Descargar archivo",
"generate": "Generar",
"makeAnother": "Hacer otro"
},
"status": {
"animationComplete": "¡Animación completa!",
"animationDead": "Solicitud de animación muerta",
"animationFailed": "La animación falló {{attemptCount}} intento(s). Lo volveré a intentar...",
"animationInProgress": "Animación en progreso",
"animationPending": "Animación pendiente...",
"animationSuccess": "Animación exitosa",
"requestingAnimation": "Solicitando animación",
"uploadingAudio": "Subiendo audio",
"uploadingImage": "Subiendo imagen"
}
}
@@ -0,0 +1,24 @@
{
"headings": {
"audio": "l'audio",
"image": "Image",
"subtitle": "Sélectionnez une image avec un visage clair et un échantillon audio, et générez une vidéo synchronisée sur les lèvres.",
"title": "Animateur de visage"
},
"inputs": {
"downloadFile": "Télécharger un fichier",
"generate": "Générer",
"makeAnother": "Faire un autre"
},
"status": {
"animationComplete": "Animation terminée !",
"animationDead": "Demande d'animation morte",
"animationFailed": "L'animation a échoué {{attemptCount}} tentatives). Je vais réessayer...",
"animationInProgress": "Animation en cours",
"animationPending": "Animation en attente...",
"animationSuccess": "Animation réussie",
"requestingAnimation": "Demander une animation",
"uploadingAudio": "Téléchargement d'audio",
"uploadingImage": "Téléchargement d'une image"
}
}
@@ -0,0 +1,24 @@
{
"headings": {
"audio": "ऑडियो",
"image": "छवि",
"subtitle": "स्पष्ट चेहरे और एक ऑडियो नमूने के साथ चयन करें और छवि बनाएं, और एक लिपसिंक किया हुआ वीडियो बनाएं।",
"title": "फेस एनिमेटर"
},
"inputs": {
"downloadFile": "डाउनलोड फ़ाइल",
"generate": "उत्पन्न",
"makeAnother": "एक और बनाने"
},
"status": {
"animationComplete": "एनिमेशन पूर्ण!",
"animationDead": "एनिमेशन अनुरोध मृत",
"animationFailed": "एनिमेशन विफल रहा {{attemptCount}} प्रयास(कोशिशें)। पुनः प्रयास करेंगे...",
"animationInProgress": "एनीमेशन प्रगति पर है",
"animationPending": "एनिमेशन लंबित है...",
"animationSuccess": "एनीमेशन सफल",
"requestingAnimation": "एनीमेशन का अनुरोध",
"uploadingAudio": "ऑडियो अपलोड हो रहा है",
"uploadingImage": "छवि अपलोड हो रही है"
}
}
@@ -0,0 +1,24 @@
{
"headings": {
"audio": "Audio",
"image": "Gambar",
"subtitle": "Pilih dan gambar dengan wajah jernih, dan sampel audio, lalu buat video lipsync.",
"title": "Animator Wajah"
},
"inputs": {
"downloadFile": "Unduh berkas",
"generate": "Menghasilkan",
"makeAnother": "Buat yang lain"
},
"status": {
"animationComplete": "Animasi selesai!",
"animationDead": "Permintaan animasi mati",
"animationFailed": "Animasi gagal {{attemptCount}} upaya. Akan mencoba lagi...",
"animationInProgress": "Animasi sedang berlangsung",
"animationPending": "Animasi tertunda...",
"animationSuccess": "Animasi berhasil",
"requestingAnimation": "Meminta Animasi",
"uploadingAudio": "Mengunggah Audio",
"uploadingImage": "Mengunggah Gambar"
}
}
@@ -0,0 +1,24 @@
{
"headings": {
"audio": "Audio",
"image": "Immagine",
"subtitle": "Seleziona un'immagine con un volto chiaro e un campione audio e genera un video sincronizzato con le labbra.",
"title": "Animatore di volti"
},
"inputs": {
"downloadFile": "Download file",
"generate": "creare",
"makeAnother": "Fanne un'altro"
},
"status": {
"animationComplete": "Animazione completata!",
"animationDead": "Richiesta di animazione morta",
"animationFailed": "Animazione fallita {{attemptCount}} tentativo(i). Ci riproverò...",
"animationInProgress": "Animazione in corso",
"animationPending": "Animazione in attesa...",
"animationSuccess": "Animazione riuscita",
"requestingAnimation": "Richiesta di animazione",
"uploadingAudio": "Caricamento dell'audio",
"uploadingImage": "Caricamento immagine"
}
}
@@ -0,0 +1,24 @@
{
"headings": {
"audio": "オーディオ",
"image": "画像",
"subtitle": "鮮明な顔と音声サンプルを選択して画像を作成し、リップシンクビデオを生成します。",
"title": "フェイスアニメーター"
},
"inputs": {
"downloadFile": "ダウンロードファイル",
"generate": "生成する",
"makeAnother": "別のものを作る"
},
"status": {
"animationComplete": "アニメーション完成!",
"animationDead": "アニメーションリクエストが終了しました",
"animationFailed": "アニメーションが失敗しました {{attemptCount}} 試み)。再試行します...",
"animationInProgress": "アニメーション進行中",
"animationPending": "アニメーションは保留中です...",
"animationSuccess": "アニメーションが成功しました",
"requestingAnimation": "アニメーションのリクエスト",
"uploadingAudio": "音声のアップロード",
"uploadingImage": "画像のアップロード"
}
}
@@ -0,0 +1,24 @@
{
"headings": {
"audio": "오디오",
"image": "영상",
"subtitle": "깨끗한 얼굴과 오디오 샘플을 선택하여 이미지화하고 립싱크된 비디오를 생성합니다.",
"title": "얼굴 애니메이터"
},
"inputs": {
"downloadFile": "파일 다운로드",
"generate": "생성하다",
"makeAnother": "다른 것을 만드세요"
},
"status": {
"animationComplete": "애니메이션 완성!",
"animationDead": "애니메이션 요청이 중단되었습니다.",
"animationFailed": "애니메이션 실패 {{attemptCount}} 시도. 다시 시도합니다...",
"animationInProgress": "애니메이션 진행 중",
"animationPending": "애니메이션 대기 중...",
"animationSuccess": "애니메이션 성공",
"requestingAnimation": "애니메이션 요청",
"uploadingAudio": "오디오 업로드",
"uploadingImage": "이미지 업로드 중"
}
}
@@ -0,0 +1,24 @@
{
"headings": {
"audio": "Áudio",
"image": "Imagem",
"subtitle": "Selecione uma imagem com um rosto nítido e uma amostra de áudio e gere um vídeo sincronizado com os lábios.",
"title": "Animador facial"
},
"inputs": {
"downloadFile": "⇬ Fazer download do arquivo",
"generate": "Gerar",
"makeAnother": "Faça outro"
},
"status": {
"animationComplete": "Animação concluída!",
"animationDead": "Solicitação de animação morta",
"animationFailed": "Falha na animação {{attemptCount}} tentativa(s). Vou tentar novamente...",
"animationInProgress": "Animação em andamento",
"animationPending": "Animação pendente...",
"animationSuccess": "Animação bem sucedida",
"requestingAnimation": "Solicitando Animação",
"uploadingAudio": "Fazendo upload de áudio",
"uploadingImage": "Carregando imagem"
}
}
@@ -0,0 +1,24 @@
{
"headings": {
"audio": "Аудио",
"image": "Изображение",
"subtitle": "Выберите изображение с четким лицом и образец звука и создайте видео с синхронизацией губ.",
"title": "Лицевой Аниматор"
},
"inputs": {
"downloadFile": "Загрузить файл",
"generate": "Генерировать",
"makeAnother": "Сделайте еще один"
},
"status": {
"animationComplete": "Анимация завершена!",
"animationDead": "Запрос на анимацию не работает",
"animationFailed": "Анимация не удалась {{attemptCount}} попытка(и). Повторю попытку...",
"animationInProgress": "Анимация в процессе",
"animationPending": "Ожидается анимация...",
"animationSuccess": "Анимация удалась",
"requestingAnimation": "Запрос анимации",
"uploadingAudio": "Загрузка аудио",
"uploadingImage": "Загрузка изображения"
}
}
@@ -0,0 +1,24 @@
{
"headings": {
"audio": "เสียง",
"image": "ภาพ",
"subtitle": "เลือกและถ่ายภาพด้วยใบหน้าที่ชัดเจนและตัวอย่างเสียง และสร้างวิดีโอแบบลิปซิงค์",
"title": "แอนิเมชั่นใบหน้า"
},
"inputs": {
"downloadFile": "ดาวน์โหลดไฟล์",
"generate": "สร้าง",
"makeAnother": "ทำอีก"
},
"status": {
"animationComplete": "แอนิเมชั่นเสร็จสมบูรณ์!",
"animationDead": "คำขอแอนิเมชันไม่ทำงาน",
"animationFailed": "แอนิเมชั่นล้มเหลว {{attemptCount}} ความพยายาม จะลองอีกครั้ง...",
"animationInProgress": "กำลังดำเนินการแอนิเมชัน",
"animationPending": "ภาพเคลื่อนไหวรอดำเนินการ...",
"animationSuccess": "แอนิเมชั่นประสบความสำเร็จ",
"requestingAnimation": "การขอภาพเคลื่อนไหว",
"uploadingAudio": "กำลังอัพโหลดเสียง",
"uploadingImage": "กำลังอัพโหลดรูปภาพ"
}
}
@@ -0,0 +1,24 @@
{
"headings": {
"audio": "Ses",
"image": "Resim",
"subtitle": "Net bir yüz ve ses örneği seçip görüntüleyin ve dudak senkronizasyonu yapılmış bir video oluşturun.",
"title": "Yüz Animatörü"
},
"inputs": {
"downloadFile": "Dosyayı indir",
"generate": "Oluştur",
"makeAnother": "Başka bir tane yap"
},
"status": {
"animationComplete": "Animasyon tamamlandı!",
"animationDead": "Animasyon isteği öldü",
"animationFailed": "Animasyon başarısız oldu {{attemptCount}} denemeler). Tekrar deneyeceğim...",
"animationInProgress": "Animasyon devam ediyor",
"animationPending": "Animasyon bekleniyor...",
"animationSuccess": "Animasyon başarılı",
"requestingAnimation": "Animasyon Talep Etme",
"uploadingAudio": "Ses Yükleniyor",
"uploadingImage": "Resim Yükleniyor"
}
}
@@ -0,0 +1,24 @@
{
"headings": {
"audio": "Âm thanh",
"image": "Hình ảnh",
"subtitle": "Chọn và tạo hình ảnh có khuôn mặt rõ ràng cũng như mẫu âm thanh rồi tạo video được hát nhép.",
"title": "khuôn mặt hoạt hình"
},
"inputs": {
"downloadFile": "Tải tập tin",
"generate": "Phát ra",
"makeAnother": "Làm một cái khác"
},
"status": {
"animationComplete": "Hoạt hình hoàn tất!",
"animationDead": "Yêu cầu hoạt ảnh đã chết",
"animationFailed": "Hoạt ảnh không thành công {{attemptCount}} (các) lần thử. Sẽ thử lại...",
"animationInProgress": "Hoạt hình đang diễn ra",
"animationPending": "Hoạt hình đang chờ xử lý...",
"animationSuccess": "Hoạt hình thành công",
"requestingAnimation": "Yêu cầu hoạt ảnh",
"uploadingAudio": "Tải lên âm thanh",
"uploadingImage": "Tải hình ảnh lên"
}
}
@@ -1,4 +1,5 @@
{
"communityButtonDiscord": "Discord",
"communityButtonTwitter": "Twitter",
"communityText": "Chúng tôi muốn trò chuyện với bạn! Hãy tham gia cùng chúng tôi trong Discord nếu bạn có bất kỳ câu hỏi nào.",
@@ -0,0 +1,24 @@
{
"headings": {
"audio": "声音的",
"image": "图像",
"subtitle": "选择具有清晰脸部和音频样本的图像,并生成口型同步视频。",
"title": "脸部动画师"
},
"inputs": {
"downloadFile": "下载文件",
"generate": "产生",
"makeAnother": "再做一个"
},
"status": {
"animationComplete": "动画完成!",
"animationDead": "动画请求失效",
"animationFailed": "动画失败 {{attemptCount}} 尝试。将重试...",
"animationInProgress": "动画正在进行中",
"animationPending": "动画待定...",
"animationSuccess": "动画成功",
"requestingAnimation": "请求动画",
"uploadingAudio": "上传音频",
"uploadingImage": "上传图片"
}
}
@@ -1 +1,4 @@
.fy-audio-uploader {
// .audio-details { position: absolute; }
}
.fy-audio-uploader > div { margin-bottom: 1rem }
@@ -1,4 +1,5 @@
import React from "react";
import { a, useTransition } from '@react-spring/web';
import { InputVcAudioPlayer } from "v2/view/_common/InputVcAudioPlayer";
import { FileActions, FileDetails, FileLabel, FileWrapper } from 'components/common';
import { faFileAudio } from "@fortawesome/pro-solid-svg-icons";
@@ -13,6 +14,7 @@ interface Props {
hideActions?: boolean;
hideClearDetails?: boolean;
inputProps?: any;
onRest?: () => void;
success?: boolean;
submit?: () => void;
working?: boolean;
@@ -21,17 +23,25 @@ interface Props {
const n = () => {};
export default function AudioInput({ blob = "", clear = n, file, hideActions, hideClearDetails, inputProps, success = false, submit = n, working = false, ...rest }: Props) {
export default function AudioInput({ blob = "", clear = n, file, hideActions, hideClearDetails, inputProps, onRest = n, success = false, submit = n, working = false, ...rest }: Props) {
const transitions = useTransition(file, {
config: { tension: 120, friction: 15 },
from: { opacity: 0 },
enter: { opacity: 1 },
leave: { opacity: 0 },
onRest,
});
return <div {...{ className: "fy-audio-uploader" }}>
<FileWrapper {...{ fileTypes, ...inputProps, panelClass: "p-3", ...rest }}>
{ file ? <FileDetails {...{ clear, hideClearDetails, icon: faFileAudio, file }}/> : <FileLabel {...{ fileTypes }}/> }
</FileWrapper>
{ file && <>
{ transitions((style, i) => i ? <a.div {...{ className: "audio-details", style }}>
<div {...{ className: "panel panel-inner rounded p-3" }}>
<InputVcAudioPlayer {...{ filename: blob as string }}/>
</div>
{ !hideActions && <FileActions {...{ clear, success, submit, working }}/> }
</> }
</a.div> : null ) }
</div>;
};
@@ -1,4 +1,5 @@
.upload-details {
position: relative;
width: 100%;
.upload-details-clear {
@@ -1,4 +1,12 @@
@import "scss/colours.scss";
@import "scss/_breakpoint.scss";
.image-upload-frame {
@include breakpoint(min-width 992px) {
aspect-ratio: 1/1;
}
}
.fy-image-uploader {
display: flex;
@@ -27,9 +35,10 @@
}
.upload-details {
top: 1rem;
left: 1rem;
width: calc(100% - 2rem);
top: 0;
width: 100%;
padding: 1rem;
border-radius: 0;
}
.image-placeholder {
@@ -1,4 +1,5 @@
import React from "react";
import React, { useState } from "react";
import { a, useSpring } from '@react-spring/web';
import { FileDetails, FileWrapper, FileLabel } from "components/common";
import "./ImageInput.scss";
@@ -11,6 +12,7 @@ interface Props {
hideActions?: boolean;
hideClearDetails?: boolean;
inputProps?: any;
onRest?: () => void;
success?: boolean;
submit?: () => void;
working?: boolean;
@@ -19,11 +21,19 @@ interface Props {
const n = () => {};
export default function ImageInput({ blob = "", clear = n, file, hideActions, hideClearDetails, inputProps, success = false, submit = n, working = false, ...rest }: Props) {
return <FileWrapper {...{ fileTypes, ...inputProps, ...rest }}>
export default function ImageInput({ blob = "", clear = n, file, hideActions, hideClearDetails, inputProps, onRest = n, success = false, submit = n, working = false, ...rest }: Props) {
const [loaded,loadedSet] = useState<number>();
const onLoad = () => loadedSet(1);
const style = useSpring({
config: { mass: 1, tension: 120, friction: 14 },
onRest,
opacity: loaded ? 1 : 0
});
return <FileWrapper {...{ fileTypes, panelClass: "image-upload-frame", ...inputProps, ...rest }}>
<div {...{ className: "fy-image-uploader" }}>
{ file ? <>
<img {...{ alt: "file preview", className: "file-preview", src: blob }} />
<a.img {...{ alt: "file preview", className: "file-preview", onLoad, src: blob, style }} />
<FileDetails {...{ file, hideClearDetails }}/>
</> : <>
<svg {...{ className: "image-placeholder", height: 400, viewBox: "0 0 300 300", width: 400 }}>
@@ -0,0 +1,45 @@
@mixin rtl($which, $step) {
&:nth-child($which) { animation-delay: $step;}
}
.fy-spinner {
stroke: white;
stroke-width: 2px;
polyline {
transform-origin: 16px 16px;
animation: feder 1s linear infinite;
stroke-dasharray: 6;
&:nth-child(2) { animation-delay: .0833s;}
&:nth-child(3) { animation-delay: .1667s;}
&:nth-child(4) { animation-delay: .25s;}
&:nth-child(5) { animation-delay: .33s;}
&:nth-child(6) { animation-delay: .4167s;}
&:nth-child(7) { animation-delay: .5s;}
&:nth-child(8) { animation-delay: .5833s;}
&:nth-child(9) { animation-delay: .66s;}
&:nth-child(10) { animation-delay: .75s;}
&:nth-child(11) { animation-delay: .833s;}
&:nth-child(12) { animation-delay: .9167s;}
}
}
@keyframes feder {
0% {
stroke-dashoffset: 0;
opacity: 0;
}
10% {
opacity: 1;
}
35% {
opacity: .25;
}
100% {
stroke-dashoffset: 6;
opacity: 0;
}
}
@@ -0,0 +1,17 @@
import React from "react";
import "./Spinner.scss";
const Line = ({ yah }:{ yah: any }) => <polyline {...{
fill: 'none',
points: `16 8 16 2`,
strokeLinecap: 'round',
strokeLinejoin: 'round',
strokeWidth: '3',
transform: `rotate(${ 30 * yah })`,
}}/>;
export default function Spinner() {
return <svg {...{ className: "fy-spinner", height: 32, viewBox: "0 0 32 32", width: 32 }}>
{ [...Array(12)].map((l,key) => <Line {...{ key, yah: key }}/>) }
</svg>;
};
@@ -0,0 +1 @@
export { default } from './Spinner';
@@ -4,6 +4,7 @@ import FileActions from './FileActions';
import FileDetails from './FileDetails';
import FileWrapper from './FileWrapper';
import FileLabel from './FileLabel';
import Spinner from './Spinner';
export {
AudioInput,
@@ -11,5 +12,6 @@ export {
FileActions,
FileDetails,
FileWrapper,
FileLabel
FileLabel,
Spinner
};
+2 -1
View File
@@ -1,6 +1,7 @@
import useFile from './useFile';
import useHover from './useHover';
import useId from './useId';
import useIdempotency from './useIdempotency';
import useLocalize from './useLocalize';
export { useFile, useId, useIdempotency, useLocalize };
export { useFile, useHover, useId, useIdempotency, useLocalize };
@@ -6,28 +6,27 @@ interface Props {
debug?: any;
onChange?: (file: any) => void;
onClear?: (x?: any) => void;
onSubmit?: (file: any) => Promise<boolean>;
onSubmit?: (file: any) => Promise<boolean | undefined> | Promise<void>;
}
export default function useFile({ debug, onChange = n, onClear = n, onSubmit = n }: Props) {
const [file, fileSet] = useState<any>(undefined);
const [blob, blobSet] = useState<string>();
const [working, workingSet] = useState(false);
const [success, successSet] = useState<boolean>(false);
const [status, statusSet] = useState<number>(0);
const inputRef = useRef<HTMLInputElement>(null);
const submit = async () => {
if (file === undefined) { return false; }
statusSet(1);
const onUploadResult = await onSubmit(file);
workingSet(true);
if (onUploadResult) { successSet(true); }
else successSet(false);
if (onUploadResult) { statusSet(2); }
else { statusSet(3); }
};
const fileChange = (inputFile?: any) => {
onChange(file);
fileSet(inputFile || null);
blobSet(inputFile ? URL.createObjectURL(inputFile) : "");
successSet(false);
statusSet(0);
};
const inputChange = ({ target = {} }: { target: any }) => {
fileChange(target.value);
@@ -36,6 +35,7 @@ export default function useFile({ debug, onChange = n, onClear = n, onSubmit = n
if (inputRef?.current?.value) inputRef.current.value = '';
fileChange();
onClear();
statusSet(0);
};
return {
@@ -46,8 +46,9 @@ export default function useFile({ debug, onChange = n, onClear = n, onSubmit = n
onChange: inputChange,
inputRef
},
status,
submit,
success,
working,
success: status === 2,
working: status === 1,
};
};
@@ -0,0 +1 @@
export { default } from './useHover';
@@ -0,0 +1,12 @@
import { useState } from 'react';
interface Props {
onMouseEnter?: (e?: any) => void;
onMouseLeave?: (e?: any) => void;
}
export default function useHover({ onMouseEnter = () => {}, onMouseLeave = () => {} }: Props) {
const [ hover, hoverSet ] = useState(false);
const onHover = (x:boolean) => (e: any) => { hoverSet(x); x ? onMouseEnter(e) : onMouseLeave(e) };
return [ hover, { onMouseEnter: onHover(true), onMouseLeave: onHover(false)}];
};
@@ -3,5 +3,5 @@ import { v4 as uuidv4 } from "uuid";
export default function useIdempotency() {
const [token, set] = useState(uuidv4());
return { reset: () => set(uuidv4()), set, token };
return { reset: () => set(uuidv4()), create: uuidv4, set, token };
};
@@ -0,0 +1,6 @@
const baseColors = {
primary: "#ea7a78",
another: "#e66462",
};
export default baseColors;
@@ -0,0 +1,4 @@
import baseColors from './baseColors';
import springs from './springs';
export { baseColors, springs }
@@ -0,0 +1,5 @@
const springs = {
soft: { mass: 1, tension: 80, friction: 10 }
};
export default springs;
+114
View File
@@ -0,0 +1,114 @@
//////////////////////////////
// Default Variables
//////////////////////////////
$Breakpoint-Settings: (
'default media': all,
'default feature': min-width,
'default pair': width,
'force all media type': false,
'to ems': false,
'transform resolutions': true,
'no queries': false,
'no query fallbacks': false,
'base font size': 16px,
'legacy syntax': false
);
$breakpoint: () !default;
//////////////////////////////
// Imports
//////////////////////////////
@import "breakpoint/settings";
@import 'breakpoint/context';
@import 'breakpoint/helpers';
@import 'breakpoint/parsers';
@import 'breakpoint/no-query';
@import 'breakpoint/respond-to';
@import "breakpoint/legacy-settings";
//////////////////////////////
// Breakpoint Mixin
//////////////////////////////
@mixin breakpoint($query, $no-query: false) {
@include legacy-settings-warning;
// Reset contexts
@include private-breakpoint-reset-contexts();
$breakpoint: breakpoint($query, false);
$query-string: map-get($breakpoint, 'query');
$query-fallback: map-get($breakpoint, 'fallback');
$private-breakpoint-context-holder: map-get($breakpoint, 'context holder') !global;
$private-breakpoint-query-count: map-get($breakpoint, 'query count') !global;
// Allow for an as-needed override or usage of no query fallback.
@if $no-query != false {
$query-fallback: $no-query;
}
@if $query-fallback != false {
$context-setter: private-breakpoint-set-context('no-query', $query-fallback);
}
// Print Out Query String
@if not breakpoint-get('no queries') {
@media #{$query-string} {
@content;
}
}
@if breakpoint-get('no query fallbacks') != false or breakpoint-get('no queries') == true {
$type: type-of(breakpoint-get('no query fallbacks'));
$print: false;
@if ($type == 'bool') {
$print: true;
}
@else if ($type == 'string') {
@if $query-fallback == breakpoint-get('no query fallbacks') {
$print: true;
}
}
@else if ($type == 'list') {
@each $wrapper in breakpoint-get('no query fallbacks') {
@if $query-fallback == $wrapper {
$print: true;
}
}
}
// Write Fallback
@if ($query-fallback != false) and ($print == true) {
$type-fallback: type-of($query-fallback);
@if ($type-fallback != 'bool') {
#{$query-fallback} & {
@content;
}
}
@else {
@content;
}
}
}
@include private-breakpoint-reset-contexts();
}
@mixin mq($query, $no-query: false) {
@include breakpoint($query, $no-query) {
@content;
}
}
+95
View File
@@ -0,0 +1,95 @@
//////////////////////////////
// Private Breakpoint Variables
//////////////////////////////
$private-breakpoint-context-holder: ();
$private-breakpoint-query-count: 0 !default;
//////////////////////////////
// Breakpoint Has Context
// Returns whether or not you are inside a Breakpoint query
//////////////////////////////
@function breakpoint-has-context() {
@if length($private-breakpoint-query-count) {
@return true;
}
@else {
@return false;
}
}
//////////////////////////////
// Breakpoint Get Context
// $feature: Input feature to get it's current MQ context. Returns false if no context
//////////////////////////////
@function breakpoint-get-context($feature) {
@if map-has-key($private-breakpoint-context-holder, $feature) {
$get: map-get($private-breakpoint-context-holder, $feature);
// Special handling of no-query from get side so /false/ prepends aren't returned
@if $feature == 'no-query' {
@if type-of($get) == 'list' and length($get) > 1 and nth($get, 1) == false {
$get: nth($get, length($get));
}
}
@return $get;
}
@else {
@if breakpoint-has-context() and $feature == 'media' {
@return breakpoint-get('default media');
}
@else {
@return false;
}
}
}
//////////////////////////////
// Private function to set context
//////////////////////////////
@function private-breakpoint-set-context($feature, $value) {
@if $value == 'monochrome' {
$feature: 'monochrome';
}
$current: map-get($private-breakpoint-context-holder, $feature);
@if $current and length($current) == $private-breakpoint-query-count {
@warn "You have already queried against `#{$feature}`. Unexpected things may happen if you query against the same feature more than once in the same `and` query. Breakpoint is overwriting the current context with `#{$value}`";
}
@if not map-has-key($private-breakpoint-context-holder, $feature) {
$v-holder: ();
@for $i from 1 to $private-breakpoint-query-count {
@if $feature == 'media' {
$v-holder: append($v-holder, breakpoint-get('default media'));
}
@else {
$v-holder: append($v-holder, false);
}
}
$v-holder: append($v-holder, $value);
$private-breakpoint-context-holder: map-merge($private-breakpoint-context-holder, ($feature: $v-holder)) !global;
}
@else {
$v-holder: map-get($private-breakpoint-context-holder, $feature);
$length: length($v-holder);
@for $i from $length to $private-breakpoint-query-count - 1 {
@if $feature == 'media' {
$v-holder: append($v-holder, breakpoint-get('default media'));
}
@else {
$v-holder: append($v-holder, false);
}
}
$v-holder: append($v-holder, $value);
$private-breakpoint-context-holder: map-merge($private-breakpoint-context-holder, ($feature: $v-holder)) !global;
}
@return true;
}
//////////////////////////////
// Private function to reset context
//////////////////////////////
@mixin private-breakpoint-reset-contexts {
$private-breakpoint-context-holder: () !global;
$private-breakpoint-query-count: 0 !global;
}
+151
View File
@@ -0,0 +1,151 @@
//////////////////////////////
// Converts the input value to Base EMs
//////////////////////////////
@function breakpoint-to-base-em($value) {
$value-unit: unit($value);
// Will convert relative EMs into root EMs.
@if breakpoint-get('base font size') and type-of(breakpoint-get('base font size')) == 'number' and $value-unit == 'em' {
$base-unit: unit(breakpoint-get('base font size'));
@if $base-unit == 'px' or $base-unit == '%' or $base-unit == 'em' or $base-unit == 'pt' {
@return base-conversion($value) / base-conversion(breakpoint-get('base font size')) * 1em;
}
@else {
@warn '#{breakpoint-get(\'base font size\')} is not set in valid units for font size!';
@return false;
}
}
@else {
@return base-conversion($value);
}
}
@function base-conversion($value) {
$unit: unit($value);
@if $unit == 'px' {
@return $value / 16px * 1em;
}
@else if $unit == '%' {
@return $value / 100% * 1em;
}
@else if $unit == 'em' {
@return $value;
}
@else if $unit == 'pt' {
@return $value / 12pt * 1em;
}
@else {
@return $value;
// @warn 'Everything is terrible! What have you done?!';
}
}
//////////////////////////////
// Returns whether the feature can have a min/max pair
//////////////////////////////
$breakpoint-min-max-features: 'color',
'color-index',
'aspect-ratio',
'device-aspect-ratio',
'device-height',
'device-width',
'height',
'monochrome',
'resolution',
'width';
@function breakpoint-min-max($feature) {
@each $item in $breakpoint-min-max-features {
@if $feature == $item {
@return true;
}
}
@return false;
}
//////////////////////////////
// Returns whether the feature can have a string value
//////////////////////////////
$breakpoint-string-features: 'orientation',
'scan',
'color',
'aspect-ratio',
'device-aspect-ratio',
'pointer',
'luminosity';
@function breakpoint-string-value($feature) {
@each $item in $breakpoint-string-features {
@if breakpoint-min-max($item) {
@if $feature == 'min-#{$item}' or $feature == 'max-#{$item}' {
@return true;
}
}
@else if $feature == $item {
@return true;
}
}
@return false;
}
//////////////////////////////
// Returns whether the feature is a media type
//////////////////////////////
$breakpoint-media-types: 'all',
'braille',
'embossed',
'handheld',
'print',
'projection',
'screen',
'speech',
'tty',
'tv';
@function breakpoint-is-media($feature) {
@each $media in $breakpoint-media-types {
@if ($feature == $media) or ($feature == 'not #{$media}') or ($feature == 'only #{$media}') {
@return true;
}
}
@return false;
}
//////////////////////////////
// Returns whether the feature can stand alone
//////////////////////////////
$breakpoint-single-string-features: 'color',
'color-index',
'grid',
'monochrome';
@function breakpoint-single-string($feature) {
@each $item in $breakpoint-single-string-features {
@if $feature == $item {
@return true;
}
}
@return false;
}
//////////////////////////////
// Returns whether the feature
//////////////////////////////
@function breakpoint-is-resolution($feature) {
$resolutions: 'device-pixel-ratio', 'dpr';
@if breakpoint-get('transform resolutions') {
$resolutions: append($resolutions, 'resolution');
}
@each $reso in $resolutions {
@if index($feature, $reso) or index($feature, 'min-#{$reso}') or index($feature, 'max-#{$reso}') {
@return true;
}
}
@return false;
}
@@ -0,0 +1,50 @@
@mixin legacy-settings-warning {
$legacyVars: (
'default-media': 'default media',
'default-feature': 'default feature',
'force-media-all': 'force all media type',
'to-ems': 'to ems',
'resolutions': 'transform resolutions',
'no-queries': 'no queries',
'no-query-fallbacks': 'no query fallbacks',
'base-font-size': 'base font size',
'legacy-syntax': 'legacy syntax'
);
@each $legacy, $new in $legacyVars {
@if global-variable-exists('breakpoint-' + $legacy) {
@warn "In order to avoid variable namspace collisions, we have updated the way to change settings for Breakpoint. Please change all instances of `$breakpoint-#{$legacy}: {{setting}}` to `@include breakpoint-set('#{$new}', {{setting}})`. Variable settings, as well as this warning will be deprecated in a future release."
}
};
//////////////////////////////
// Hand correct each setting
//////////////////////////////
@if global-variable-exists('breakpoint-default-media') and $breakpoint-default-media != breakpoint-get('default media') {
@include breakpoint-set('default media', $breakpoint-default-media);
}
@if global-variable-exists('breakpoint-default-feature') and $breakpoint-default-feature != breakpoint-get('default feature') {
@include breakpoint-set('default feature', $breakpoint-default-feature);
}
@if global-variable-exists('breakpoint-force-media-all') and $breakpoint-force-media-all != breakpoint-get('force all media type') {
@include breakpoint-set('force all media type', $breakpoint-force-media-all);
}
@if global-variable-exists('breakpoint-to-ems') and $breakpoint-to-ems != breakpoint-get('to ems') {
@include breakpoint-set('to ems', $breakpoint-to-ems);
}
@if global-variable-exists('breakpoint-resolutions') and $breakpoint-resolutions != breakpoint-get('transform resolutions') {
@include breakpoint-set('transform resolutions', $breakpoint-resolutions);
}
@if global-variable-exists('breakpoint-no-queries') and $breakpoint-no-queries != breakpoint-get('no queries') {
@include breakpoint-set('no queries', $breakpoint-no-queries);
}
@if global-variable-exists('breakpoint-no-query-fallbacks') and $breakpoint-no-query-fallbacks != breakpoint-get('no query fallbacks') {
@include breakpoint-set('no query fallbacks', $breakpoint-no-query-fallbacks);
}
@if global-variable-exists('breakpoint-base-font-size') and $breakpoint-base-font-size != breakpoint-get('base font size') {
@include breakpoint-set('base font size', $breakpoint-base-font-size);
}
@if global-variable-exists('breakpoint-legacy-syntax') and $breakpoint-legacy-syntax != breakpoint-get('legacy syntax') {
@include breakpoint-set('legacy syntax', $breakpoint-legacy-syntax);
}
}
+15
View File
@@ -0,0 +1,15 @@
@function breakpoint-no-query($query) {
@if type-of($query) == 'list' {
$keyword: nth($query, 1);
@if type-of($keyword) == 'string' and ($keyword == 'no-query' or $keyword == 'no query' or $keyword == 'fallback') {
@return nth($query, 2);
}
@else {
@return false;
}
}
@else {
@return false;
}
}
+215
View File
@@ -0,0 +1,215 @@
//////////////////////////////
// Import Parser Pieces
//////////////////////////////
@import "parsers/query";
@import "parsers/single";
@import "parsers/double";
@import "parsers/triple";
@import "parsers/resolution";
$Memo-Exists: function-exists(memo-get) and function-exists(memo-set);
//////////////////////////////
// Breakpoint Function
//////////////////////////////
@function breakpoint($query, $contexts...) {
$run: true;
$return: ();
// Grab the Memo Output if Memoization can be a thing
@if $Memo-Exists {
$return: memo-get(breakpoint, breakpoint $query $contexts);
@if $return != null {
$run: false;
}
}
@if not $Memo-Exists or $run {
// Internal Variables
$query-string: '';
$query-fallback: false;
$return: ();
// Reserve Global Private Breakpoint Context
$holder-context: $private-breakpoint-context-holder;
$holder-query-count: $private-breakpoint-query-count;
// Reset Global Private Breakpoint Context
$private-breakpoint-context-holder: () !global;
$private-breakpoint-query-count: 0 !global;
// Test to see if it's a comma-separated list
$or-list: if(list-separator($query) == 'comma', true, false);
@if ($or-list == false and breakpoint-get('legacy syntax') == false) {
$query-string: breakpoint-parse($query);
}
@else {
$length: length($query);
$last: nth($query, $length);
$query-fallback: breakpoint-no-query($last);
@if ($query-fallback != false) {
$length: $length - 1;
}
@if (breakpoint-get('legacy syntax') == true) {
$mq: ();
@for $i from 1 through $length {
$mq: append($mq, nth($query, $i), comma);
}
$query-string: breakpoint-parse($mq);
}
@else {
$query-string: '';
@for $i from 1 through $length {
$query-string: $query-string + if($i == 1, '', ', ') + breakpoint-parse(nth($query, $i));
}
}
}
$return: ('query': $query-string,
'fallback': $query-fallback,
'context holder': $private-breakpoint-context-holder,
'query count': $private-breakpoint-query-count
);
@if length($contexts) > 0 and nth($contexts, 1) != false {
@if $query-fallback != false {
$context-setter: private-breakpoint-set-context('no-query', $query-fallback);
}
$context-map: ();
@each $context in $contexts {
$context-map: map-merge($context-map, ($context: breakpoint-get-context($context)));
}
$return: map-merge($return, (context: $context-map));
}
// Reset Global Private Breakpoint Context
$private-breakpoint-context-holder: () !global;
$private-breakpoint-query-count: 0 !global;
@if $Memo-Exists {
$holder: memo-set(breakpoint, breakpoint $query $contexts, $return);
}
}
@return $return;
}
//////////////////////////////
// General Breakpoint Parser
//////////////////////////////
@function breakpoint-parse($query) {
// Increase number of 'and' queries
$private-breakpoint-query-count: $private-breakpoint-query-count + 1 !global;
// Set up Media Type
$query-print: '';
$force-all: ((breakpoint-get('force all media type') == true) and (breakpoint-get('default media') == 'all'));
$empty-media: true;
@if ($force-all == true) or (breakpoint-get('default media') != 'all') {
// Force the print of the default media type if (force all is true and default media type is all) or (default media type is not all)
$query-print: breakpoint-get('default media');
$empty-media: false;
}
$query-resolution: false;
$query-holder: breakpoint-parse-query($query);
// Loop over each parsed out query and write it to $query-print
$first: true;
@each $feature in $query-holder {
$length: length($feature);
// Parse a single feature
@if ($length == 1) {
// Feature is currently a list, grab the actual value
$feature: nth($feature, 1);
// Media Type must by convention be the first item, so it's safe to flat override $query-print, which right now should only be the default media type
@if (breakpoint-is-media($feature)) {
@if ($force-all == true) or ($feature != 'all') {
// Force the print of the default media type if (force all is true and default media type is all) or (default media type is not all)
$query-print: $feature;
$empty-media: false;
// Set Context
$context-setter: private-breakpoint-set-context(media, $query-print);
}
}
@else {
$parsed: breakpoint-parse-single($feature, $empty-media, $first);
$query-print: '#{$query-print} #{$parsed}';
$first: false;
}
}
// Parse a double feature
@else if ($length == 2) {
@if (breakpoint-is-resolution($feature) != false) {
$query-resolution: $feature;
}
@else {
$parsed: null;
// If it's a string/number pair,
// we check to see if one is a single-string value,
// then we parse it as a normal double
$alpha: nth($feature, 1);
$beta: nth($feature, 2);
@if breakpoint-single-string($alpha) or breakpoint-single-string($beta) {
$parsed: breakpoint-parse-single($alpha, $empty-media, $first);
$query-print: '#{$query-print} #{$parsed}';
$first: false;
$parsed: breakpoint-parse-single($beta, $empty-media, $first);
$query-print: '#{$query-print} #{$parsed}';
}
@else {
$parsed: breakpoint-parse-double($feature, $empty-media, $first);
$query-print: '#{$query-print} #{$parsed}';
$first: false;
}
}
}
// Parse a triple feature
@else if ($length == 3) {
$parsed: breakpoint-parse-triple($feature, $empty-media, $first);
$query-print: '#{$query-print} #{$parsed}';
$first: false;
}
}
@if ($query-resolution != false) {
$query-print: breakpoint-build-resolution($query-print, $query-resolution, $empty-media, $first);
}
// Loop through each feature that's been detected so far and append 'false' to the the value list to increment their counters
@each $f, $v in $private-breakpoint-context-holder {
$v-holder: $v;
$length: length($v-holder);
@if length($v-holder) < $private-breakpoint-query-count {
@for $i from $length to $private-breakpoint-query-count {
@if $f == 'media' {
$v-holder: append($v-holder, breakpoint-get('default media'));
}
@else {
$v-holder: append($v-holder, false);
}
}
}
$private-breakpoint-context-holder: map-merge($private-breakpoint-context-holder, ($f: $v-holder)) !global;
}
@return $query-print;
}
+82
View File
@@ -0,0 +1,82 @@
////////////////////////
// Default the Breakpoints variable
////////////////////////
$breakpoints: () !default;
$BREAKPOINTS: () !default;
////////////////////////
// Respond-to API Mixin
////////////////////////
@mixin respond-to($context, $no-query: false) {
@if length($breakpoints) > 0 and length($BREAKPOINTS) == 0 {
@warn "In order to avoid variable namespace collisions, we have updated the way to add breakpoints for respond-to. Please change all instances of `$breakpoints: add-breakpoint()` to `@include add-breakpoint()`. The `add-breakpoint()` function will be deprecated in a future release.";
$BREAKPOINTS: $breakpoints !global;
$breakpoints: () !global;
}
@if type-of($BREAKPOINTS) != 'map' {
// Just in case someone writes gibberish to the $breakpoints variable.
@warn "Your breakpoints aren't a map! `respond-to` expects a map. Please check the value of $BREAKPOINTS variable.";
@content;
}
@else if map-has-key($BREAKPOINTS, $context) {
@include breakpoint(map-get($BREAKPOINTS, $context), $no-query) {
@content;
}
}
@else if not map-has-key($BREAKPOINTS, $context) {
@warn "`#{$context}` isn't a defined breakpoint! Please add it using `$breakpoints: add-breakpoint(`#{$context}`, $value);`";
@content;
}
@else {
@warn "You haven't created any breakpoints yet! Make some already! `@include add-breakpoint($name, $bkpt)`";
@content;
}
}
//////////////////////////////
// Add Breakpoint to Breakpoints
// TODO: Remove function in next release
//////////////////////////////
@function add-breakpoint($name, $bkpt, $overwrite: false) {
$output: ($name: $bkpt);
@if length($breakpoints) == 0 {
@return $output;
}
@else {
@if map-has-key($breakpoints, $name) and $overwrite != true {
@warn "You already have a breakpoint named `#{$name}`, please choose another breakpoint name, or pass in `$overwrite: true` to overwrite the previous breakpoint.";
@return $breakpoints;
}
@else if not map-has-key($breakpoints, $name) or $overwrite == true {
@return map-merge($breakpoints, $output);
}
}
}
@mixin add-breakpoint($name, $bkpt, $overwrite: false) {
$output: ($name: $bkpt);
@if length($BREAKPOINTS) == 0 {
$BREAKPOINTS: $output !global;
}
@else {
@if map-has-key($BREAKPOINTS, $name) and $overwrite != true {
@warn "You already have a breakpoint named `#{$name}`, please choose another breakpoint name, or pass in `$overwrite: true` to overwrite the previous breakpoint.";
$BREAKPOINTS: $BREAKPOINTS !global;
}
@else if not map-has-key($BREAKPOINTS, $name) or $overwrite == true {
$BREAKPOINTS: map-merge($BREAKPOINTS, $output) !global;
}
}
}
@function get-breakpoint($name: false) {
@if $name == false {
@return $BREAKPOINTS;
}
@else {
@return map-get($BREAKPOINTS, $name);
}
}
+71
View File
@@ -0,0 +1,71 @@
//////////////////////////////
// Has Setting
//////////////////////////////
@function breakpoint-has($setting) {
@if map-has-key($breakpoint, $setting) {
@return true;
}
@else {
@return false;
}
}
//////////////////////////////
// Get Settings
//////////////////////////////
@function breakpoint-get($setting) {
@if breakpoint-has($setting) {
@return map-get($breakpoint, $setting);
}
@else {
@return map-get($Breakpoint-Settings, $setting);
}
}
//////////////////////////////
// Set Settings
//////////////////////////////
@function breakpoint-set($setting, $value) {
@if (str-index($setting, '-') or str-index($setting, '_')) and str-index($setting, ' ') == null {
@warn "Words in Breakpoint settings should be separated by spaces, not dashes or underscores. Please replace dashes and underscores between words with spaces. Settings will not work as expected until changed.";
}
$breakpoint: map-merge($breakpoint, ($setting: $value)) !global;
@return true;
}
@mixin breakpoint-change($setting, $value) {
$breakpoint-change: breakpoint-set($setting, $value);
}
@mixin breakpoint-set($setting, $value) {
@include breakpoint-change($setting, $value);
}
@mixin bkpt-change($setting, $value) {
@include breakpoint-change($setting, $value);
}
@mixin bkpt-set($setting, $value) {
@include breakpoint-change($setting, $value);
}
//////////////////////////////
// Remove Setting
//////////////////////////////
@function breakpoint-reset($settings...) {
@if length($settings) == 1 {
$settings: nth($settings, 1);
}
@each $setting in $settings {
$breakpoint: map-remove($breakpoint, $setting) !global;
}
@return true;
}
@mixin breakpoint-reset($settings...) {
$breakpoint-reset: breakpoint-reset($settings);
}
@mixin bkpt-reset($settings...) {
$breakpoint-reset: breakpoint-reset($settings);
}
@@ -0,0 +1,33 @@
//////////////////////////////
// Import Pieces
//////////////////////////////
@import "double/default-pair";
@import "double/double-string";
@import "double/default";
@function breakpoint-parse-double($feature, $empty-media, $first) {
$parsed: '';
$leader: '';
// If we're forcing
@if not ($empty-media) or not ($first) {
$leader: 'and ';
}
$first: nth($feature, 1);
$second: nth($feature, 2);
// If we've got two numbers, we know we need to use the default pair because there are no media queries that has a media feature that is a number
@if type-of($first) == 'number' and type-of($second) == 'number' {
$parsed: breakpoint-parse-default-pair($first, $second);
}
// If they are both strings, we send it through the string parser
@else if type-of($first) == 'string' and type-of($second) == 'string' {
$parsed: breakpoint-parse-double-string($first, $second);
}
// If it's a string/number pair, we parse it as a normal double
@else {
$parsed: breakpoint-parse-double-default($first, $second);
}
@return $leader + $parsed;
}
@@ -0,0 +1,82 @@
@function breakpoint-parse-query($query) {
// Parse features out of an individual query
$feature-holder: ();
$query-holder: ();
$length: length($query);
@if $length == 2 {
// If we've got a string/number, number/string, check to see if it's a valid string/number pair or two singles
@if (type-of(nth($query, 1)) == 'string' and type-of(nth($query, 2)) == 'number') or (type-of(nth($query, 1)) == 'number' and type-of(nth($query, 2)) == 'string') {
$number: '';
$value: '';
@if type-of(nth($query, 1)) == 'string' {
$number: nth($query, 2);
$value: nth($query, 1);
}
@else {
$number: nth($query, 1);
$value: nth($query, 2);
}
// If the string value can be a single value, check to see if the number passed in is a valid input for said single value. Fortunately, all current single-value options only accept unitless numbers, so this check is easy.
@if breakpoint-single-string($value) {
@if unitless($number) {
$feature-holder: append($value, $number, space);
$query-holder: append($query-holder, $feature-holder, comma);
@return $query-holder;
}
}
// If the string is a media type, split the query
@if breakpoint-is-media($value) {
$query-holder: append($query-holder, nth($query, 1));
$query-holder: append($query-holder, nth($query, 2));
@return $query-holder;
}
// If it's not a single feature, we're just going to assume it's a proper string/value pair, and roll with it.
@else {
$feature-holder: append($value, $number, space);
$query-holder: append($query-holder, $feature-holder, comma);
@return $query-holder;
}
}
// If they're both numbers, we assume it's a double and roll with that
@else if (type-of(nth($query, 1)) == 'number' and type-of(nth($query, 2)) == 'number') {
$feature-holder: append(nth($query, 1), nth($query, 2), space);
$query-holder: append($query-holder, $feature-holder, comma);
@return $query-holder;
}
// If they're both strings and neither are singles, we roll with that.
@else if (type-of(nth($query, 1)) == 'string' and type-of(nth($query, 2)) == 'string') {
@if not breakpoint-single-string(nth($query, 1)) and not breakpoint-single-string(nth($query, 2)) {
$feature-holder: append(nth($query, 1), nth($query, 2), space);
$query-holder: append($query-holder, $feature-holder, comma);
@return $query-holder;
}
}
}
@else if $length == 3 {
// If we've got three items and none is a list, we check to see
@if type-of(nth($query, 1)) != 'list' and type-of(nth($query, 2)) != 'list' and type-of(nth($query, 3)) != 'list' {
// If none of the items are single string values and none of the values are media values, we're good.
@if (not breakpoint-single-string(nth($query, 1)) and not breakpoint-single-string(nth($query, 2)) and not breakpoint-single-string(nth($query, 3))) and ((not breakpoint-is-media(nth($query, 1)) and not breakpoint-is-media(nth($query, 2)) and not breakpoint-is-media(nth($query, 3)))) {
$feature-holder: append(nth($query, 1), nth($query, 2), space);
$feature-holder: append($feature-holder, nth($query, 3), space);
$query-holder: append($query-holder, $feature-holder, comma);
@return $query-holder;
}
// let's check to see if the first item is a media type
@else if breakpoint-is-media(nth($query, 1)) {
$query-holder: append($query-holder, nth($query, 1));
$feature-holder: append(nth($query, 2), nth($query, 3), space);
$query-holder: append($query-holder, $feature-holder);
@return $query-holder;
}
}
}
// If it's a single item, or if it's not a special case double or triple, we can simply return the query.
@return $query;
}
@@ -0,0 +1,31 @@
@import "resolution/resolution";
@function breakpoint-build-resolution($query-print, $query-resolution, $empty-media, $first) {
$leader: '';
// If we're forcing
@if not ($empty-media) or not ($first) {
$leader: 'and ';
}
@if breakpoint-get('transform resolutions') and $query-resolution {
$resolutions: breakpoint-make-resolutions($query-resolution);
$length: length($resolutions);
$query-holder: '';
@for $i from 1 through $length {
$query: '#{$query-print} #{$leader}#{nth($resolutions, $i)}';
@if $i == 1 {
$query-holder: $query;
}
@else {
$query-holder: '#{$query-holder}, #{$query}';
}
}
@return $query-holder;
}
@else {
// Return with attached resolution
@return $query-print;
}
}
@@ -0,0 +1,26 @@
//////////////////////////////
// Import Pieces
//////////////////////////////
@import "single/default";
@function breakpoint-parse-single($feature, $empty-media, $first) {
$parsed: '';
$leader: '';
// If we're forcing
@if not ($empty-media) or not ($first) {
$leader: 'and ';
}
// If it's a single feature that can stand alone, we let it
@if (breakpoint-single-string($feature)) {
$parsed: $feature;
// Set Context
$context-setter: private-breakpoint-set-context($feature, $feature);
}
// If it's not a stand alone feature, we pass it off to the default handler.
@else {
$parsed: breakpoint-parse-default($feature);
}
@return $leader + '(' + $parsed + ')';
}
@@ -0,0 +1,36 @@
//////////////////////////////
// Import Pieces
//////////////////////////////
@import "triple/default";
@function breakpoint-parse-triple($feature, $empty-media, $first) {
$parsed: '';
$leader: '';
// If we're forcing
@if not ($empty-media) or not ($first) {
$leader: 'and ';
}
// separate the string features from the value numbers
$string: null;
$numbers: null;
@each $val in $feature {
@if type-of($val) == string {
$string: $val;
}
@else {
@if type-of($numbers) == 'null' {
$numbers: $val;
}
@else {
$numbers: append($numbers, $val);
}
}
}
$parsed: breakpoint-parse-triple-default($string, nth($numbers, 1), nth($numbers, 2));
@return $leader + $parsed;
}
@@ -0,0 +1,21 @@
@function breakpoint-parse-default-pair($first, $second) {
$default: breakpoint-get('default pair');
$min: '';
$max: '';
// Sort into min and max
$min: min($first, $second);
$max: max($first, $second);
// Set Context
$context-setter: private-breakpoint-set-context(min-#{$default}, $min);
$context-setter: private-breakpoint-set-context(max-#{$default}, $max);
// Make them EMs if need be
@if (breakpoint-get('to ems') == true) {
$min: breakpoint-to-base-em($min);
$max: breakpoint-to-base-em($max);
}
@return '(min-#{$default}: #{$min}) and (max-#{$default}: #{$max})';
}
@@ -0,0 +1,22 @@
@function breakpoint-parse-double-default($first, $second) {
$feature: '';
$value: '';
@if type-of($first) == 'string' {
$feature: $first;
$value: $second;
}
@else {
$feature: $second;
$value: $first;
}
// Set Context
$context-setter: private-breakpoint-set-context($feature, $value);
@if (breakpoint-get('to ems') == true) {
$value: breakpoint-to-base-em($value);
}
@return '(#{$feature}: #{$value})'
}
@@ -0,0 +1,22 @@
@function breakpoint-parse-double-string($first, $second) {
$feature: '';
$value: '';
// Test to see which is the feature and which is the value
@if (breakpoint-string-value($first) == true) {
$feature: $first;
$value: $second;
}
@else if (breakpoint-string-value($second) == true) {
$feature: $second;
$value: $first;
}
@else {
@warn "Neither #{$first} nor #{$second} is a valid media query name.";
}
// Set Context
$context-setter: private-breakpoint-set-context($feature, $value);
@return '(#{$feature}: #{$value})';
}
@@ -0,0 +1,60 @@
@function breakpoint-make-resolutions($resolution) {
$length: length($resolution);
$output: ();
@if $length == 2 {
$feature: '';
$value: '';
// Find which is number
@if type-of(nth($resolution, 1)) == 'number' {
$value: nth($resolution, 1);
}
@else {
$value: nth($resolution, 2);
}
// Determine min/max/standard
@if index($resolution, 'min-resolution') {
$feature: 'min-';
}
@else if index($resolution, 'max-resolution') {
$feature: 'max-';
}
$standard: '(#{$feature}resolution: #{$value})';
// If we're not dealing with dppx,
@if unit($value) != 'dppx' {
$base: 96dpi;
@if unit($value) == 'dpcm' {
$base: 243.84dpcm;
}
// Write out feature tests
$webkit: '';
$moz: '';
$webkit: '(-webkit-#{$feature}device-pixel-ratio: #{$value / $base})';
$moz: '(#{$feature}-moz-device-pixel-ratio: #{$value / $base})';
// Append to output
$output: append($output, $standard, space);
$output: append($output, $webkit, space);
$output: append($output, $moz, space);
}
@else {
$webkit: '';
$moz: '';
$webkit: '(-webkit-#{$feature}device-pixel-ratio: #{$value / 1dppx})';
$moz: '(#{$feature}-moz-device-pixel-ratio: #{$value / 1dppx})';
$fallback: '(#{$feature}resolution: #{$value / 1dppx * 96dpi})';
// Append to output
$output: append($output, $standard, space);
$output: append($output, $webkit, space);
$output: append($output, $moz, space);
$output: append($output, $fallback, space);
}
}
@return $output;
}
@@ -0,0 +1,13 @@
@function breakpoint-parse-default($feature) {
$default: breakpoint-get('default feature');
// Set Context
$context-setter: private-breakpoint-set-context($default, $feature);
@if (breakpoint-get('to ems') == true) and (type-of($feature) == 'number') {
@return '#{$default}: #{breakpoint-to-base-em($feature)}';
}
@else {
@return '#{$default}: #{$feature}';
}
}
@@ -0,0 +1,18 @@
@function breakpoint-parse-triple-default($feature, $first, $second) {
// Sort into min and max
$min: min($first, $second);
$max: max($first, $second);
// Set Context
$context-setter: private-breakpoint-set-context(min-#{$feature}, $min);
$context-setter: private-breakpoint-set-context(max-#{$feature}, $max);
// Make them EMs if need be
@if (breakpoint-get('to ems') == true) {
$min: breakpoint-to-base-em($min);
$max: breakpoint-to-base-em($max);
}
@return '(min-#{$feature}: #{$min}) and (max-#{$feature}: #{$max})';
}
@@ -6,6 +6,7 @@ import { LoginPage } from "./pages/login/LoginPage";
import { ModerationFc } from "./pages/moderation/moderation_main/ModerationFc";
import { ModerationIpBanListFc } from "./pages/moderation/moderation_ip_ban_list/ModerationIpBanListFc";
import { ModerationViewIpBanFc } from "./pages/moderation/moderation_view_ip_ban/ModerationViewIpBanFc";
import { Media } from "./pages/media";
import { LipsyncEditor } from "./pages/lipsync";
import { FooterNav } from "./nav/FooterNav";
import { TopNav } from "./nav/TopNav";
@@ -80,7 +81,6 @@ import { VoiceConversionModelListItem } from "@storyteller/components/src/api/vo
import { CommunityCommissionsPage } from "./pages/contest/CommunityCommissionsPage";
import { ProductUsageInfoPage } from "./pages/product_usage_info/ProductUsageInfoPage";
import { TestingPage } from "./pages/testing/TestingPage";
import { FaceAnimationPage } from "./pages/face-animation/FaceAnimationPage";
interface Props {
sessionWrapper: SessionWrapper;
@@ -432,8 +432,8 @@ class PageContainer extends React.Component<Props, State> {
<CreateCategoryPage sessionWrapper={this.props.sessionWrapper} />
</Route>
<Route path="/dev-lipsync">
<LipsyncEditor />
<Route path="/media/:token">
<Media/>
</Route>
<Route path="/moderation/user/list">
@@ -544,14 +544,12 @@ class PageContainer extends React.Component<Props, State> {
</Route>
<Route path="/face-animation">
<FaceAnimationPage
sessionSubscriptionsWrapper={
this.props.sessionSubscriptionsWrapper
}
enqueueInferenceJob={this.props.enqueueInferenceJob}
inferenceJobs={this.props.inferenceJobs}
inferenceJobsByCategory={this.props.inferenceJobsByCategory}
/>
<LipsyncEditor {...{
enqueueInferenceJob: this.props.enqueueInferenceJob,
sessionSubscriptionsWrapper: this.props.sessionSubscriptionsWrapper,
inferenceJobs: this.props.inferenceJobs,
inferenceJobsByCategory: this.props.inferenceJobsByCategory
}} />
</Route>
<Route path="/commissions">
@@ -50,7 +50,7 @@ function FaceAnimationPage(props: Props) {
if (EnqueueFaceAnimationIsSuccess(result)) {
props.enqueueInferenceJob(
result.inference_job_token,
result.inference_job_token!,
FrontendInferenceJobType.FaceAnimation
);
}
@@ -0,0 +1,17 @@
.fy-dynamic-button {
height: 3rem;
appearance: none;
border-radius: 0.5rem;
border: none;
color: #fff;
outline: none;
padding: 10px 20px;
position: relative;
width: fit-content;
overflow: hidden;
.button-slide {
// position: absolute;
width: fit-content;
}
}
@@ -0,0 +1,37 @@
import React from "react";
import { a, useSpring, useTransition } from "@react-spring/web";
import { useHover } from "hooks";
import { baseColors } from "resources";
import "./DynamicButton.scss"
interface Props {
children?: string|JSX.Element|JSX.Element[];
className?: string;
disabled?: boolean;
key?: boolean;
[x:string]: any;
}
export default function DynamicButton({ children, className, disabled, index = 0, key, slides = [], ...rest }: Props) {
const [hover, hoverProps = {}] = useHover({});
const style = useSpring({
backgroundColor: hover ? baseColors.primary : baseColors.another,
config: { tension: 130, friction: 20 },
opacity: disabled ? .5 : 1
});
const transitions = useTransition(index, {
config: { tension: 130, friction: 20 },
from: { opacity: 0, position: "absolute" },
enter: { opacity: 1, position: "relative" },
leave: { opacity: 0, position: "absolute" },
});
return <a.button {...{ className: `fy-dynamic-button${ className ? " " + className : "" }`, disabled, style, ...hoverProps, ...rest }}>
{ transitions(({ opacity, position }, content, s, i) => {
return <a.div {...{ className: `button-slide ${i}`, style: { opacity, position: position as any } }}>
{ slides[content] }
</a.div>;
}) }
</a.button>;
};
@@ -0,0 +1,91 @@
import React, { useEffect, useState } from 'react';
import { animated } from '@react-spring/web';
import {
GetPendingTtsJobCount,
GetPendingTtsJobCountIsOk,
GetPendingTtsJobCountSuccessResponse,
} from "@storyteller/components/src/api/tts/GetPendingTtsJobCount";
import { FrontendInferenceJobType, InferenceJob } from "@storyteller/components/src/jobs/InferenceJob";
import { BucketConfig } from "@storyteller/components/src/api/BucketConfig";
import { JobState } from "@storyteller/components/src/jobs/JobStates";
import { Analytics } from "common/Analytics";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faDownload } from "@fortawesome/free-solid-svg-icons";
interface Props {
audioProps: any;
imageProps: any;
style: any;
enqueueInferenceJob: any;
sessionSubscriptionsWrapper: any;
inferenceJobsByCategory: any;
t: any;
}
const DEFAULT_QUEUE_REFRESH_INTERVAL_MILLIS = 15000;
export default function FaceAnimatorSuccess({ audioProps, imageProps, style, enqueueInferenceJob, sessionSubscriptionsWrapper, inferenceJobsByCategory, t }: Props) {
const [pending, pendingSet] = useState<GetPendingTtsJobCountSuccessResponse>({
success: true,
pending_job_count: 0,
cache_time: new Date(0), // NB: Epoch is used for vector clock's initial state
refresh_interval_millis: DEFAULT_QUEUE_REFRESH_INTERVAL_MILLIS,
});
const inferenceJobs = inferenceJobsByCategory.get(FrontendInferenceJobType.FaceAnimation)!;
const mediaLink = (path: string) => new BucketConfig().getGcsUrl(path);
const processStatus = (job: InferenceJob) => {
switch (job.jobState) {
case JobState.PENDING:
case JobState.UNKNOWN: return t("status.animationPending");
case JobState.STARTED: return t("status.animationInProgress");
case JobState.ATTEMPT_FAILED: return t("status.animationPending",{ attemptCount: job.attemptCount });
case JobState.COMPLETE_FAILURE:
case JobState.DEAD: return t("status.animationDead");
case JobState.COMPLETE_SUCCESS: return t("status.animationSuccess");
}
};
useEffect(() => {
const fetch = async () => {
const response = await GetPendingTtsJobCount();
if (GetPendingTtsJobCountIsOk(response)) {
if (
response.cache_time.getTime() > pending.cache_time.getTime()
) {
pendingSet(response);
}
}
};
// TODO: We're having an outage and need to lower this.
//const interval = setInterval(async () => fetch(), 15000);
const refreshInterval = Math.max(
DEFAULT_QUEUE_REFRESH_INTERVAL_MILLIS,
pending.refresh_interval_millis
);
const interval = setInterval(async () => fetch(), refreshInterval);
fetch();
return () => clearInterval(interval);
}, [pending]);
return <animated.div {...{ className: "lipsync-success", style }}>
<div {...{ className: "face-animator-results" }}>{
inferenceJobs.map((job: any) => !job.maybeResultToken ?
<div>
<h3>{ processStatus(job) }</h3>
<span {...{ className: "job-id" }}>id: { job.jobToken }</span>
</div> : <div>
<h3>{ t("status.animationComplete") }</h3>
<span {...{ className: "job-id" }}>id: { job.jobToken }</span>
<a {...{
className: "btn btn-primary w-100 mt-4",
download: `fakeyou-${job.jobToken}.mp4`,
href: mediaLink(job.maybeResultPublicBucketMediaPath),
onClick:() => Analytics.voiceConversionClickDownload()
}}>
<FontAwesomeIcon icon={faDownload} className="me-2" />
{ t("inputs.downloadFile") }
</a>
</div>).reverse()
}</div>
</animated.div>;
};
@@ -0,0 +1,90 @@
import React from "react";
import { animated, useSpring } from "@react-spring/web";
import { Spinner } from "components/common";
import { springs } from "resources";
import DynamicButton from "./DynamicButton";
const ProgressLi = ({
children,
disabled = false,
}: {
children?: any;
disabled?: boolean;
}) => {
const style = useSpring({
...springs.soft,
opacity: disabled ? 0.25 : 1,
});
return (
<animated.li {...{ style }}>
<svg>
<circle {...{ cx: 16, cy: 16, r: 15, strokeWidth: "2" }} />
{
<polyline
{...{
fill: "none",
points: "9.5 18 14.5 22 22.5 12",
strokeLinecap: "round",
strokeLinejoin: "round",
strokeWidth: "4",
}}
/>
}
</svg>
{children}
</animated.li>
);
};
export default function FaceAnimatorTitle({ ...rest }) {
const {
audioProps,
audioReady,
imageProps,
imageReady,
indexSet,
page,
submit,
t,
} = rest;
const noAudio = !audioReady || !audioProps.file;
const noImg = !imageReady || !imageProps.file;
const incomplete = noAudio || noImg;
const working = imageProps.working && audioProps.working;
const slides = [t("inputs.generate"), <Spinner />, t("inputs.makeAnother")];
const onClick = () => {
if (page === 2) {
imageProps.clear();
audioProps.clear();
indexSet(0);
} else if (!incomplete && !working) submit();
};
return (
<div {...{ className: "progress-header" }}>
<h1
{...{ className: "fw-bold text-center text-md-start progress-heading" }}
>
{t("headings.title")}
</h1>
<ul {...{ className: "async-progress-tracker py-3 py-lg-0" }}>
<ProgressLi {...{ disabled: noImg }}>{t("headings.image")}</ProgressLi>
<ProgressLi {...{ disabled: noAudio }}>
{t("headings.audio")}
</ProgressLi>
</ul>
<DynamicButton
{...{
className: "face-animation-submit",
disabled: incomplete || working,
onClick,
slides,
index: page,
}}
/>
<p {...{ className: "progress-description" }}>{t("headings.subtitle")}</p>
</div>
);
}
@@ -1,88 +1,104 @@
@import "scss/_breakpoint.scss";
$under992: max-width 992px;
.lipsync-editor {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
grid-gap: 1rem;
padding-top: 2rem;
.grid-heading, .grid-square {
&:nth-child(odd) {
padding-left: 1rem;
}
&:nth-child(even) {
padding-right: 1rem;
}
}
.grid-heading {
display: flex;
align-items: center;
// background-color: blue;
// padding-left: 1rem;
h5 {
margin: 1rem 0 0;
// background: red;
line-height: 1em;
}
}
.grid-square {
aspect-ratio: 1;
padding-bottom: 1rem;
// &:nth-child(odd) {
// padding-left: 1rem;
// }
// &:nth-child(even) {
// padding-right: 1rem;
// }
}
// div.grid-square { aspect-ratio: 1 }
.fy-image-uploader { height: 100%; }
.lipsync-audio {
label { height: 100% }
}
.media-input-column {
display: inline-block;
}
@include breakpoint($under992) {
.media-input-column:first-of-type {
margin-bottom: 1rem;
}
}
}
.progress-header {
display: flex;
align-items: center;
margin-bottom: 1rem;
display: flex;
flex-wrap: wrap;
align-items: center;
// padding: 1rem;
.progress-heading {
display: inline-block;
margin: auto 1rem auto 0;
}
.progress-heading {
display: inline-block;
margin: auto 1rem auto 0;
}
.async-progress-tracker {
display: inline-flex;
justify-content: space-around;
flex-grow: 1;
margin-right: 1rem;
padding-left: 0;
.async-progress-tracker {
display: inline-flex;
justify-content: space-around;
flex-grow: 1;
margin-right: 1rem;
padding-left: 0;
li {
display: flex;
align-items: center;
font-size: 1.25rem;
font-weight: 500;
}
li {
display: flex;
align-items: center;
font-size: 1.25rem;
font-weight: 500;
}
svg {
border: 2px solid #fff;
border-radius: 50%;
margin-right: .5rem;
height: 2rem;
width: 2rem;
stroke: white;
}
}
svg {
margin-right: 0.5rem;
height: 2rem;
width: 2rem;
stroke: white;
}
}
button {
margin: 0 0 0 auto;
background-color: #ccccff;
// flex-grow: 1;
width: fit-content;
}
}
.progress-description {
margin-top: 1rem;
width: 100%;
}
button {
margin: 0 0 0 auto;
background-color: #ccccff;
width: fit-content;
@include breakpoint($under992) {
margin: 1rem 0 0;
width: 100%;
.button-slide {
width: 100%;
}
}
}
}
.lipsync-panel {
position: relative;
overflow: hidden;
}
.lipsync-working,
.lipsync-working-notice {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
}
.lipsync-working-notice {
flex-direction: column;
margin: 5rem;
.working-title {
white-space: nowrap;
}
}
.lipsync-success {
padding: 0.5rem 1rem 1rem;
h3 {
margin: 1rem auto 0 0;
}
.job-id {
color: rgba(#fff, 0.66);
font-size: 0.75rem;
}
}
@@ -1,76 +1,262 @@
import React from "react";
import { useFile } from "hooks";
import { AudioInput, ImageInput } from "components/common";
import { PageHeader } from "components/layout";
import './LipsyncEditor.scss';
import React, { useState } from "react";
import { animated, useTransition } from "@react-spring/web";
import { v4 as uuidv4 } from "uuid";
import { useFile, useLocalize } from "hooks";
import { AudioInput, ImageInput, Spinner } from "components/common";
import { springs } from "resources";
import {
UploadAudio,
// UploadAudioIsOk,
// UploadAudioRequest,
} from "@storyteller/components/src/api/upload/UploadAudio";
import {
UploadImage,
// UploadImageIsOk,
// UploadImageRequest,
} from "@storyteller/components/src/api/upload/UploadImage";
import {
EnqueueFaceAnimation,
// EnqueueFaceAnimationIsSuccess,
// EnqueueFaceAnimationRequest,
} from "@storyteller/components/src/api/face_animation/EnqueueFaceAnimation";
// import { FrontendInferenceJobType } from "@storyteller/components/src/jobs/InferenceJob";
import FaceAnimatorTitle from "./FaceAnimatorTitle";
import FaceAnimatorSuccess from "./FaceAnimatorSuccess";
import "./LipsyncEditor.scss";
import { SessionSubscriptionsWrapper } from "@storyteller/components/src/session/SessionSubscriptionsWrapper";
import {
FrontendInferenceJobType,
InferenceJob,
} from "@storyteller/components/src/jobs/InferenceJob";
// interface Props {
// audioFile?: any;
// imageFile?: any;
// }
interface LipSyncProps {
audioProps: any;
children?: any;
imageProps: any;
index: number;
toggle: any;
style: any;
enqueueInferenceJob: any;
sessionSubscriptionsWrapper: any;
t: any;
inferenceJobsByCategory: any;
}
interface EditorProps {
sessionSubscriptionsWrapper: SessionSubscriptionsWrapper;
enqueueInferenceJob: (
jobToken: string,
frontendInferenceJobType: FrontendInferenceJobType
) => void;
inferenceJobs: Array<InferenceJob>;
inferenceJobsByCategory: Map<FrontendInferenceJobType, Array<InferenceJob>>;
}
const ProgressCheck = ({ disabled = false }: {disabled?: boolean}) => <svg>
{ !disabled && <polyline {...{
fill: 'none',
points: '7 16 12 20 20 10',
strokeLinecap: 'round',
strokeLinejoin: 'round',
strokeWidth: '4',
}}/> }
</svg>;
const softSpring = { config: { mass: 1, tension: 80, friction: 10 } };
const Title = ({ ...rest }) => {
const { audioFile, imageFile } = rest;
// const InputPage = ({ audioProps, imageProps, toggle, style, t }: LipSyncProps ) => {
return <div {...{ className: 'progress-header' }}>
<h1 {...{ className: "fw-bold text-center text-md-start progress-heading" }}>
Generate a lipsync
</h1>
<ul {...{ className: 'async-progress-tracker' }}>
<li>
<ProgressCheck {...{ disabled: imageFile }}/>
Image
</li>
<li>
<ProgressCheck {...{ disabled: audioFile }}/>
Audio
</li>
</ul>
<button className="btn btn-primary">
Generate
</button>
</div>
// return <animated.div {...{ className: "lipsync-editor", style }}>
// <div {...{ className: "grid-heading" }}>
// <h5>{ t("headings.image") }</h5>
// </div>
// <div {...{ className: "grid-heading" }}>
// <h5>{ t("headings.audio") }</h5>
// </div>
// <div {...{ className: "grid-square lipsync-audio col-lg-6" }}>
// <ImageInput {...{ ...imageProps, onRest: () => toggle.image(imageProps.file ? true : false) }}/>
// </div>
// <div {...{ className: "grid-square col-lg-6" }}>
// <AudioInput {...{ ...audioProps, onRest: (p:any,c:any,item:any,l:any) => {
// toggle.audio(!!audioProps.file);
// }, hideActions: true } }/>
// </div>
// </animated.div>};
const InputPage = ({
audioProps,
imageProps,
toggle,
style,
t,
}: LipSyncProps) => {
return (
<animated.div {...{ className: "lipsync-editor row", style }}>
<div {...{ className: "media-input-column col-lg-6" }}>
<h5>{t("headings.image")}</h5>
<ImageInput
{...{
...imageProps,
onRest: () => toggle.image(imageProps.file ? true : false),
}}
/>
</div>
<div {...{ className: "media-input-column col-lg-6" }}>
<h5>{t("headings.audio")}</h5>
<AudioInput
{...{
...audioProps,
onRest: (p: any, c: any, item: any, l: any) => {
toggle.audio(!!audioProps.file);
},
hideActions: true,
}}
/>
</div>
</animated.div>
);
};
export default function LipsyncEditor({ ...rest }) {
const audioProps = useFile({ debug: 'audio useFile' });
const imageProps = useFile({});
const Working = ({ audioProps, imageProps, index, style, t }: LipSyncProps) => {
const workStatus = [
"",
t("status.uploadingAudio"),
t("status.uploadingImage"),
t("status.requestingAnimation"),
"",
];
const transitions = useTransition(index, {
...springs.soft,
from: { opacity: 0, position: "absolute" },
enter: { opacity: 1, position: "relative" },
leave: { opacity: 0, position: "absolute" },
});
return (
<animated.div {...{ className: "lipsync-working", style }}>
<div {...{ className: "lipsync-working-notice" }}>
<h2 {...{ className: "working-title" }}>
{" "}
{transitions(({ opacity, position }, i) => {
return (
<animated.span
{...{
className: "working-title-text",
style: { opacity, position: position as any },
}}
>
{workStatus[index]} ...
</animated.span>
);
})}
</h2>
<Spinner />
</div>
</animated.div>
);
};
const subText = "Select an image with a clear face, or one of our existing templates, then choose either text to speech or uploaded audio(eg. music). Then you can generate a beautifully lipsynced video.";
export default function LipsyncEditor({
enqueueInferenceJob,
sessionSubscriptionsWrapper,
inferenceJobsByCategory,
...rest
}: EditorProps) {
const { t } = useLocalize("FaceAnimator");
// the ready states are set by functions which run after the upload input animation is completed, which then illuminates the respective checkmark in a staggered way to draw attention to the workflow, and reduces concurrent animations
const [imageReady, imageReadySet] = useState<boolean>(false);
const [audioReady, audioReadySet] = useState<boolean>(false);
const readyMedia = (m: number) => (t: boolean) =>
[imageReadySet, audioReadySet][m](t);
const audioProps = useFile({}); // contains upload inout state and controls, see docs
const imageProps = useFile({}); // contains upload inout state and controls, see docs
const [index, indexSet] = useState<number>(0); // index = slideshow slide position
const makeRequest = (mode: number) => ({
uuid_idempotency_token: uuidv4(),
file: mode ? imageProps.file : audioProps.file,
source: "file",
type: mode ? "image" : "audio",
});
const upImageAndMerge = async (audio: any) => ({
audio,
image: await UploadImage(makeRequest(1)),
});
const submit = async () => {
if (!audioProps.file) return false;
indexSet(1); // set audio working page
UploadAudio(makeRequest(0)) // start audio (0) upload
.then((res) => {
if ("upload_token" in res) {
indexSet(2); // set image working page
}
return upImageAndMerge(res); // start image (1) upload, replace with Upload(imageRequest)
})
.then((responses) => {
if ("upload_token" in responses.image) {
indexSet(3); // set face animator API working page
return EnqueueFaceAnimation({
uuid_idempotency_token: uuidv4(),
audio_source: {
maybe_media_upload_token: responses.audio.upload_token,
},
image_source: {
maybe_media_upload_token: responses.image.upload_token,
},
});
}
})
.then((res) => {
if (res && res.inference_job_token) {
enqueueInferenceJob(
res.inference_job_token,
FrontendInferenceJobType.FaceAnimation
);
indexSet(4); // set face animator API success page
}
})
.catch((e) => {
return { success: false };
});
};
const page = index === 0 ? 0 : index === 4 ? 2 : 1;
const headerProps = {
childProps: { audioFile: !audioProps.file, imageFile: !imageProps.file },
titleComponent: Title,
subText,
showButtons: false
audioProps,
audioReady,
imageProps,
imageReady,
indexSet,
page,
submit,
t,
};
return <div>
<PageHeader { ...headerProps }/>
<div {...{ className: "container" }}>
<div {...{ className: "lipsync-editor panel" }}>
<div {...{ className: "grid-heading" }}>
<h5>Image</h5>
</div>
<div {...{ className: "grid-heading" }}>
<h5>Audio</h5>
</div>
<div {...{ className: "grid-square lipsync-audio" }}>
<ImageInput {...imageProps}/>
</div>
<div {...{ className: "grid-square" }}>
<AudioInput {...{ ...audioProps, hideActions: true } }/>
</div>
</div>
const transitions = useTransition(index, {
...softSpring,
from: { opacity: 0, position: "absolute" },
enter: { opacity: 1, position: "relative" },
leave: { opacity: 0, position: "absolute" },
});
return (
<div {...{ className: "container-panel pt-4" }}>
<div {...{ className: "panel lipsync-panel p-3 py-4 p-md-4" }}>
<FaceAnimatorTitle {...headerProps} />
{transitions((style, i) => {
const Page = [InputPage, Working, FaceAnimatorSuccess][page];
return Page ? (
<Page
{...{
audioProps,
imageProps,
enqueueInferenceJob,
sessionSubscriptionsWrapper,
inferenceJobsByCategory,
index,
t,
toggle: { audio: readyMedia(1), image: readyMedia(0) },
style,
}}
/>
) : (
<></>
);
})}
</div>
</div>;
};
</div>
);
}
@@ -0,0 +1,3 @@
import media from './media';
export { media as Media };
@@ -0,0 +1,11 @@
import React from 'react';
interface Props {
value?: any;
}
export default function media({ value }: Props) {
return <div>
<h1>Media</h1>
</div>;
};
@@ -48,7 +48,7 @@ function TestingPage(props: Props) {
let result = await EnqueueFaceAnimation(request);
if (EnqueueFaceAnimationIsSuccess(result)) {
if (EnqueueFaceAnimationIsSuccess(result) && result.inference_job_token) {
props.enqueueInferenceJob(
result.inference_job_token,
FrontendInferenceJobType.FaceAnimation
+60
View File
@@ -4206,6 +4206,64 @@ __metadata:
languageName: node
linkType: hard
"@react-spring/animated@npm:~9.7.3":
version: 9.7.3
resolution: "@react-spring/animated@npm:9.7.3"
dependencies:
"@react-spring/shared": ~9.7.3
"@react-spring/types": ~9.7.3
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
checksum: 468942ca3a11c02c3e56def26b2da9dd10ddbed548004245c4ac309cce00b58d971e781abed67db0d652f72737eaa73766ea9a43b8ef3b08a7ed2eddc04d4c39
languageName: node
linkType: hard
"@react-spring/core@npm:~9.7.3":
version: 9.7.3
resolution: "@react-spring/core@npm:9.7.3"
dependencies:
"@react-spring/animated": ~9.7.3
"@react-spring/shared": ~9.7.3
"@react-spring/types": ~9.7.3
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
checksum: 8a80a07276458fd14099320eda824e58a11ce3a9b03a5c9cd3f4252adb4d26da04ee5caf5cbc961199f55c2d58a99638d5ea292cdb6aa029208dbab741b5c531
languageName: node
linkType: hard
"@react-spring/shared@npm:~9.7.3":
version: 9.7.3
resolution: "@react-spring/shared@npm:9.7.3"
dependencies:
"@react-spring/types": ~9.7.3
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
checksum: 912b5e567eb5345c9a6c8e8c0c2d69b1f411af72a0685b95831809c267c89846a31341ca071f284ace98b3cb5de647054dc76f6ace81d6379513eaf96b52f195
languageName: node
linkType: hard
"@react-spring/types@npm:^9.7.3, @react-spring/types@npm:~9.7.3":
version: 9.7.3
resolution: "@react-spring/types@npm:9.7.3"
checksum: f47b81fe556464aa54a78603311cb584d6a0f03088522229afb058265bbe2ade2095a55ec7f4e960c3b9cceaa5d47865bc41fc6643c0f5f4bd3d8650203d8389
languageName: node
linkType: hard
"@react-spring/web@npm:^9.7.3":
version: 9.7.3
resolution: "@react-spring/web@npm:9.7.3"
dependencies:
"@react-spring/animated": ~9.7.3
"@react-spring/core": ~9.7.3
"@react-spring/shared": ~9.7.3
"@react-spring/types": ~9.7.3
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
checksum: 7f5cd05b2314b7f2f715e1926abcf9aa0a539399b222ab34e989144f48350adfcd2edab65d41425570f72c57f602fc6994d6730fbeed902171ac527b630a8a9b
languageName: node
linkType: hard
"@rollup/plugin-babel@npm:^5.2.0":
version: 5.3.0
resolution: "@rollup/plugin-babel@npm:5.3.0"
@@ -4362,6 +4420,7 @@ __metadata:
"@fortawesome/pro-regular-svg-icons": ^6.2.1
"@fortawesome/pro-solid-svg-icons": ^6.2.1
"@fortawesome/react-fontawesome": ^0.2.0
"@react-spring/web": ^9.7.3
"@storyteller/components": 0.1.0
"@testing-library/jest-dom": ^4.2.4
"@testing-library/react": ^9.5.0
@@ -4509,6 +4568,7 @@ __metadata:
version: 0.0.0-use.local
resolution: "@storyteller/root@workspace:."
dependencies:
"@react-spring/types": ^9.7.3
craco: ^0.0.3
lerna: ^4.0.0
yarn: ^1.22.19