iArt.ai
AI 모션 그래픽 소프트웨어, 누구나 사용할 수 있는 After Effects 대안, 텍스트 프롬프트로 몇 분 안에 애니메이션·설명 영상 제작
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
The cinematic social network. Not a TikTok clone — the test for every feature here is
"does this make watching video feel more cinematic?" FRAME is landscape-first, not
16:9-only — 16:9, 21:9 Cinema, and 16:10 are all first-class, with 3:2 and 4:3 wired
into the same taxonomy for future launch. Swipe-first, full-screen, built for
filmmakers, drone pilots, and anyone who shoots wide.README 전문 보기 ↗The cinematic social network. Not a TikTok clone — the test for every feature here is "does this make watching video feel more cinematic?" FRAME is landscape-first, not 16:9-only — 16:9, 21:9 Cinema, and 16:10 are all first-class, with 3:2 and 4:3 wired into the same taxonomy for future launch. Swipe-first, full-screen, built for filmmakers, drone pilots, and anyone who shoots wide.
This repo is the Phase 1 MVP, now through its Version 2 transformation (see Version 2 — Cinematic Social Network below): the core swipe feed, upload flow with landscape enforcement, Explore/Discovery, Collections, profile, inbox, and auth scaffolding. No monetization, TV app, or AI features yet — see Roadmap.
| Layer | Choice | Why |
|---|---|---|
| Framework | Next.js 16 (App Router) + TypeScript | RSC for fast first paint on feed/profile pages, route handlers double as the API layer |
| Styling | Tailwind CSS v4 | Design tokens live in globals.css via @theme, no separate config file to drift |
| Animation | Framer Motion | Shared motion vocabulary in lib/motion.ts — cinematic focus-pull, Director Mode chrome fade, sheet springs — nothing snaps |
| State | Zustand | One tiny global store (src/store/player-store.ts) for mute + active video; everything else is local component state |
| Data fetching | TanStack Query | Wired via QueryProvider, ready for real endpoints — feed currently reads mock data directly |
| Auth + DB | Supabase | Postgres for users/videos/follows/likes, @supabase/ssr for cookie-based sessions |
| Video delivery | Cloudflare Stream (recommended) | Adaptive HLS out of the box, cheaper per-minute than Mux at MVP scale, revisit Mux once creator analytics depth matters more than cost |
| Object storage | Cloudflare R2 | Avatars, banners, thumbnails, LUT/preset downloads — no egress fees pairs well with Stream |
npm install
cp .env.local.example .env.local # fill in Supabase + Cloudflare keys when ready
npm run dev
The app reads real Supabase data and requires the configured environment. Bundled
demo profiles, videos, and collections have been removed; empty accounts show
empty states. Uploads require Cloudflare Stream credentials. Category choices
live in src/lib/categories.ts. Private share-token playback remains unimplemented.
The retired seed identities are recorded in scripts/demo-manifest.json.
node --env-file=.env.local scripts/remove-demo-data.mjs previews those exact
records; --apply verifies identities, snapshots their content locally, then
removes them and associated cascading interactions. It refuses non-demo videos
or changed identities. This is not a general-purpose user cleanup command.
FRAME's identity changed: from "TikTok for landscape video" to a cinematic social network — every decision measured against "does this make watching video feel more cinematic?" This was a critique-then-transform pass, not a feature bolt-on. The full critique and prioritization is preserved in the session's plan file; the summary:
What was removed or softened (TikTok patterns that didn't fit the new identity):
ActionRail.tsx), not a separate
animated element flying up the screen.What's new:
player-store.ts (directorMode), rendered in VideoCard.tsx,
FeedTabs.tsx, SideRail.tsx, BottomNav.tsx; reset on feed unmount
(SwipeFeed.tsx) so it can never leak into other routes.scroll-snap-stop: always in globals.css keeps a
fast fling from skipping past multiple videos — "one scene at a time," not doomscroll
velocity.lib/audio.ts's fadeVolume() ramps volume via
requestAnimationFrame instead of snapping; video never begins or ends abruptly
once unmuted. Spatial Audio/Dolby Atmos need encode-side support Cloudflare Stream
doesn't expose today — that's a Phase 5 dependency, documented honestly rather than
faked client-side.VideoDetails in types.ts, VideoDetailsSheet.tsx),
reached by tapping "Shot details" under a caption rather than adding another icon
to an already-busy rail.Collection type, a cover-art rail
(CollectionsRail.tsx), a /collections/[id] detail page, and a real destination
for the Profile "Collections" tab (previously a dead stub) backed by a new
savedCollections set in engagement-store.ts, following the exact pattern
already used for likes/saves/follows.Deliberately not built yet (documented, not faked):
videos.type: 'vod' | 'live' is a natural reserved column whenever
this gets built; no code changes needed today to keep the door open.roadmap.md Milestone 5.A different mode from the last two rounds: "FRAME is no longer in feature development... Do not invent unnecessary features. Refine. Simplify. Polish." The full audit and reasoning behind every call here lives in BETA_READINESS.md — what got removed, what got improved, what was deliberately left alone, and two launch checklists. This section is the changelog; that file is the report.
Removed, not added — the point of this pass:
VideoOptionsSheet's "Copy link" action, which duplicated ActionRail's
dedicated Share button.VideoCard — redundant with Explore already being
one tap away in the persistent nav.Hidden Gems (lowest-like-count videos) in favor of New Voices (creators
below a follower threshold) — a real, distinct signal that serves creator
retention directly, unlike a rail that was really just re-sorting the same 5
videos. The brief's other four suggested rail types (Award Winners, Trending
Collections, Weekend Watchlist, Editor's Choice) were deliberately not
built — with 4 creators and 5 videos they'd have no real signal behind them,
which is exactly the "endless algorithmic clutter" the brief warns against.Fixed — broken promises and real friction:
app/(app)/settings) — interests, sound-on-by-default, sign out — and
onboarding gained a real "why FRAME exists" step instead of dropping straight
into a category picker.Category didn't support.upload-draft-store.ts autosaves title/description/category (metadata only —
a File object can't survive a reload without IndexedDB, which is out of
scope here; documented rather than half-built).Built — each tied to a specific brief section:
/watch/[id] — a real generateMetadata + opengraph-image.tsx
(Next's built-in ImageResponse, no external service) so a shared link
actually unfurls as a branded card (poster, title, creator, duration) in
iMessage/Slack/Twitter. ActionRail's Share button points here now instead of
the internal /?v= deep link, which stays as the in-app navigation mechanism.
Deliberately scoped to public videos only — /watch/[id] never looks at
privateVideos; a private video's only public surface remains the
token-gated, revocable /s/[token].lib/search.ts): matches category, location, camera, lens,
equipment, tags, and badges — not just title/username — and surfaces matching
Collections above the video grid, not just videos.lib/quality.ts): a 0–100 score from resolution, supported
aspect ratio, badge signals, and an engagement-per-follower proxy — never
shown as a number anywhere (per spec). It matters through one real
integration: FRAME Certified in computeBadges() can now be earned by score
as well as manually authored, so it's a live signal instead of a fixed list.
Honest gap: completion rate and watch retention (which the spec also asks
for) need real playback analytics this app doesn't have yet — the proxy is
named as a proxy, not disguised as the real thing.Premiere type in
types.ts documenting scheduled-release, reminder, and countdown-chat-window
semantics. No UI, no store, nothing wired — a schema for later, not a feature
now.<MotionConfig reducedMotion="user"> at the root layout —
every existing Framer Motion animation in the app now respects the OS-level
reduced-motion setting from one place, rather than needing prefers-reduced-motion
checks threaded through every component individually.Investigated, not changed — the brief asked whether comments should exist at
all, or whether something like "Creator Notes" would feel more intentional.
Worth noting: Creator Notes already exists — VideoDetails.creatorNotes,
shipped in Version 2, surfaced in VideoDetailsSheet. The existing
CommentDrawer is already an opt-in bottom sheet, not an always-visible endless
thread — it was never the pattern the brief worries about. Replacing a working,
expected feature with something unproven days before a creator beta is real risk
for no demonstrated upside, so it stays as-is. Full reasoning in
BETA_READINESS.md.
src/
app/
(app)/ route group with the shared nav shell
page.tsx Home — swipe feed
explore/
upload/
inbox/
profile/
onboarding/ interest picker, gates the (app) shell on first visit
login/ outside the shell (no bottom nav)
components/
feed/ SwipeFeed, VideoCard, FeedTabs, ActionRail, VideoOverlay,
CommentDrawer, VideoOptionsSheet
upload/ UploadDropzone + UploadRejection (multi-ratio gate + Rotate/Crop/Guidelines)
explore/ profile/ inbox/ nav/
ui/ Logo, Avatar, BadgeRow
lib/
aspect-ratio.ts the ratio taxonomy — single source of truth for classification
video-validation.ts upload accept/reject logic, built on aspect-ratio.ts
badges.ts merges editorial + computed badges (never asserts a ratio badge
the real pixels don't back up)
supabase/ browser + server clients, middleware session refresh
mock-data.ts types.ts
store/
player-store.ts mute + active-video
engagement-store.ts likes/saves/follows, persisted to localStorage
comments-store.ts locally-added comments, persisted to localStorage
onboarding-store.ts first-visit interest picker, persisted to localStorage
SwipeFeed is a native CSS scroll-snap container (snap-y snap-mandatory), not a
custom drag/swipe reimplementation — that gets momentum scrolling, rubber-banding,
and mouse-wheel-advances-to-next-video for free, which also satisfies the desktop
"wheel scrolls to next video" requirement without extra code. An IntersectionObserver
tracks which card is ≥60% visible and marks it active; only the active <video>
plays, everything else is paused and reset — critical for battery/bandwidth once this
is real HLS instead of mock MP4s.
Where I pushed back on the brief: "every video is 16:9" and "no black bars" are in tension on a portrait phone screen — a 16:9 video literally cannot fill a 9:19.5 viewport without either cropping the frame or leaving empty space above/below it. Cropping would violate "every video is 16:9" (you'd be showing a different crop than the creator uploaded). So instead of hard black bars, the letterbox space is filled with a blurred, scaled-up copy of the video's poster frame — the Apple TV app / Netflix mobile technique. It reads as intentional and cinematic rather than as an unfilled gap, and the actual frame the creator shot is never cropped or stretched.
lib/aspect-ratio.ts defines the taxonomy as five non-overlapping ratio bands
(minRatio–maxRatio, not a single nominal value) so real-world encodes classify
correctly — e.g. 21:9 in the spec is nominally 2.33:1, but the actual example
resolutions (2560×1080, 3440×1440, 5120×2160) range 2.37–2.39, so the band is
[2.20, 2.45]. 16:9/21:9/16:10 are enabled: true (Primary Support); 3:2/4:3 are
enabled: false (Optional Future Support) — flipping that one flag is the entire
launch step for either, since classification, badges, and the discovery filter all
key off the same table. Gaps between bands (e.g. 1.85:1 "flat" cinema) are
intentionally unsupported until added here, not silently rounded into a neighbor.
The player needed almost no changes. It renders <video className="w-full h-full object-contain"> inside a full-bleed centered flex box, sized from the video's real
intrinsic pixels — never a hardcoded 16:9 box. That means 21:9 letterboxes and 16:10
pillarboxes for free, using the exact same blurred-backdrop technique already built
for 16:9. The only real player change was constraining the overlay chrome (captions,
action rail, tabs) to a max-w-[1920px] centered column so it stays visually anchored
to the video instead of floating at the edges of an ultra-wide monitor.
Grid thumbnails respect the real ratio too. TrendingGrid (Explore/Profile) used
to be a fixed aspect-video CSS grid; it's now a CSS-columns masonry layout with each
card sized via style={{ aspectRatio: width/height }} — a 21:9 upload genuinely reads
as a wide card, not a 16:9 box with a cropped thumbnail. Pure CSS, no JS masonry library.
UploadDropzone loads the dropped file into an off-DOM <video> element, reads
videoWidth/videoHeight from loadedmetadata, and calls checkUpload() before
anything reaches a form or an upload request. Two distinct rejection paths, because
they warrant different copy and different recovery options:
not-landscape (portrait or square) → UploadRejection explains FRAME is
landscape-only and offers three real options: Rotate 90° (genuinely swaps
width/height and re-validates — this is the actual fix for the common "phone
recorded sideways, exported portrait" case, not just a message), Crop to a
supported ratio (shows a real centered-crop preview per target ratio, nothing
applied until confirmed — the spec is explicit: do not auto-crop), and
guidelines (the supported ratios + example resolutions, pulled from the same
ASPECT_RATIOS table so it can't drift from what's actually enforced).unsupported-ratio (landscape, but outside every enabled band — e.g. 1.85:1)
→ softer copy, plus the nearest enabled ratio by distance from the closer band edge.The upload screen shows aspect ratio, resolution, and duration immediately — a
browser <video> element can read those from loadedmetadata. FPS, codec, and
bitrate are shown as "detected after upload" rather than faked: the browser's
media APIs don't expose them reliably cross-browser, and in the real pipeline they
only become available once Cloudflare Stream finishes probing the file server-side.
Showing placeholder values here would be lying about what the client actually knows.
lib/badges.ts computes a video's badge list by merging two sources: editorial/
creator-declared badges (video.badges — Drone, Shot on RED, FRAME Certified) and
badges computed from the real encode (4K if the resolution qualifies, 21:9 Cinema
if classifyAspectRatio says so). This split matters: a "21:9 Cinema" badge can
never be manually asserted on a video that doesn't actually play back that wide —
it's derived, not editable. Known gap: all current mock videos are genuinely
16:9 (they're reused stock demo clips), so the 21:9/16:10 Explore filters and the
21:9 Cinema badge are correctly empty in this build — they'll populate the moment
a real widescreen file is uploaded, since validation runs against real probed
dimensions, not mock data.
A creator's Profile has a third tab, Private, alongside Videos and Collections
(ProfileTabs.tsx). Private videos (mock-data.ts's privateVideos) never appear
in the feed, Explore, or the public profile grid — the only way to reach one is a
share link the creator generates from that tab (CreateShareLinkSheet.tsx), with a
chosen TTL (1 hour / 24 hours / 7 days). The link resolves at /s/[token], outside
the app shell, and:
ExpiredLinkNotice.tsx).Revocation is immediate and independent of the expiry timer — a creator can kill a link early from the same sheet that created it, and each link tracks a basic view count (incremented once per valid page load).
Real implementation vs. this build: lib/share-links.ts documents the intended
shape — a server-minted, DB-backed token (not a client-generated one) checked
against a share_links table on every request, with Cloudflare Stream's own
signed-URL/requireSignedURLs feature layered underneath to control actual
playback access (short-lived, minted fresh per page load, independent of the
share link's own longer TTL). In this Phase 1 build, share-links-store.ts is
localStorage-backed like the other engagement stores — which means a link only
resolves in the same browser that created it. That's enough to demo and test the
full creation → view → expiry → revocation flow, but it does not yet work for
actually sending a link to someone else's device — that requires Milestone 1 of
roadmap.md.
No live database exists yet in Phase 1 (state lives in mock-data.ts and
localStorage-backed Zustand stores) — this documents the schema the current
types already assume, so Phase 2's actual Postgres migration doesn't have to
re-derive it:
videos
id, creator_id, playback_url, poster_url, title, description, category,
width int, height int, -- source of truth for aspect ratio; never store
-- a separate "aspect_ratio" enum column, derive
-- it from width/height like the client does
visibility enum('public','private'), -- private videos are excluded from every
-- feed/Explore/profile-grid query, full stop —
-- reachable only via a valid share_links row
badges text[], -- editorial only (Drone, Shot on RED, FRAME Certified);
-- computed badges (4K, 21:9 Cinema) stay derived,
-- never written to a row
duration_seconds, sound_name, likes/comments/shares/saves counters,
created_at
creators
id, username, display_name, avatar_url, banner_url, bio, website,
verified bool, followers/following/total_views (denormalized counters
or materialized views once real-time accuracy matters)
follows (user_id, creator_id)
likes (user_id, video_id)
saves (user_id, video_id)
comments (id, video_id, user_id, text, created_at)
watch_progress (user_id, video_id, position_seconds) -- Phase 4, cross-device resume
share_links (token pk, video_id, creator_id, expires_at, revoked_at, view_count,
created_at) -- token is server-generated;
-- `/s/[token]` checks this
-- table, not client state
width/height as the aspect-ratio source of truth (rather than a stored ratio
label) is deliberate — it's the same principle lib/badges.ts relies on client-side:
derive, don't duplicate, so nothing can claim a ratio the pixels don't back up.
Updated 2026-09-14 — most of this section described a mock-data-only phase of the project that has since ended. Corrected below; only share links and crop confirmation are still genuinely stubbed.
/api/uploads/route.ts
mints a real Cloudflare Stream direct-upload (TUS) session, the client PUTs
the file straight to Cloudflare, and /api/webhooks/stream/route.ts
updates the video to ready once Stream confirms the encode (also where
the actual short/monetise-eligibility reclassification happens against
Stream's authoritative duration, closing a forged-client-duration bypass
fixed 2026-09-14). Caveat: CLOUDFLARE_STREAM_API_TOKEN has never
actually been provisioned with a real value — the code is complete but has
likely never run end-to-end against a real Stream account.UploadRejection's crop preview is a real, accurate centered-crop preview,
but "Continue cropped" doesn't cut pixels client-side — no canvas/WebCodecs
re-encode exists yet. It stages the decision and shows it back to the
uploader; the real crop is meant to happen server-side alongside
transcoding but does not yet./api/comments/route.ts), not a direct client insert. Onboarding
interests are still localStorage-only (onboarding-store.ts) and, as of
2026-09-14, are also not read by any feed-ranking code — the Settings copy
describing them as shaping Discover was corrected to stop claiming that.share-links-store.ts remains
localStorage-backed; a link literally cannot be opened on a different
device until it's backed by the real share_links table + Stream signed
URLs (see above). Still the
highest-priority remaining item in this section.Feature evolution after launch. For the checklist to actually get from this repo to a live production site, see roadmap.md.
The target audience is professional film people — working cinematographers, editors, and creators who care about proper aspect ratios and real technical craft, not the widest possible casual audience. "16:9 fanatics" is the literal framing: landscape-only by design (16:9/21:9/16:10, no portrait), and every UI decision from here on should read as professional film tooling that happens to be a social app, not a TikTok clone with a different crop.
Already-built work that's consistent with this direction, worth treating as
precedent rather than one-offs: the tag taxonomy's gear facet (real
manufacturer/model tags — Sony FX3, ARRI Alexa 35, specific lens/mount
combinations — with auto-inheritance, not a generic "camera" checkbox), the
multi-ratio landscape upload gate with rotate/crop recovery, the cinematic
dark visual language, and the clip-trim sheet's video-preview scrubber
(ClipCreateSheet.tsx) — a real, if minimal, NLE-style interaction rather
than a bare two-handle slider. Future UI work (the tag-picker dropdowns,
any further editing-adjacent surfaces) should keep measuring itself against
"would a working DP/editor find this credible" rather than "does this match
a generic social app pattern."
watch_progress table keyed by user+video, shared across mobile/desktop/TV.savedVideosengagement-store.tsCreator gained optional statement,
equipment, availableForHire; ProfileHeader.tsx shows them when present, and a
new FeaturedWork.tsx surfaces a creator's top video and a collection their work
appears in, above the tabs.lib/motion.ts centralizes duration/easing constants
(DURATION, EASE_OUT, SHEET_SPRING, FOCUS_PULL_TRANSITION,
CHROME_FADE_TRANSITION) so every animation reaches for the same considered
vocabulary instead of an inline spring config invented per component.