jbilcke-hf HF staff commited on
Commit
29f166e
·
1 Parent(s): 655b911

improve AiTube algorithm

Browse files
src/app/interface/like-button/generic.tsx CHANGED
@@ -56,7 +56,7 @@ export function GenericLikeButton({
56
  <div>{
57
  isLikedByUser ? <RiThumbUpFill /> : <RiThumbUpLine />
58
  }</div>
59
- <div>{numberOfLikes}</div>
60
  </div>
61
  <div className={cn(
62
  `flex flex-row items-center justify-center`,
@@ -74,7 +74,7 @@ export function GenericLikeButton({
74
  <div>{
75
  isDislikedByUser ? <RiThumbDownFill /> : <RiThumbDownLine />
76
  }</div>
77
- <div>{numberOfDislikes}</div>
78
  </div>
79
  </div>
80
  )
 
56
  <div>{
57
  isLikedByUser ? <RiThumbUpFill /> : <RiThumbUpLine />
58
  }</div>
59
+ <div>{Math.max(0, numberOfLikes)}</div>
60
  </div>
61
  <div className={cn(
62
  `flex flex-row items-center justify-center`,
 
74
  <div>{
75
  isDislikedByUser ? <RiThumbDownFill /> : <RiThumbDownLine />
76
  }</div>
77
+ <div>{Math.max(0, numberOfDislikes0}</div>
78
  </div>
79
  </div>
80
  )
src/app/interface/like-button/index.tsx CHANGED
@@ -41,31 +41,42 @@ export function LikeButton({
41
  if (!huggingfaceApiKey) { return null }
42
 
43
  const handleLike = huggingfaceApiKey ? () => {
44
- // we use premeptive update
 
45
  setRating({
46
  ...rating,
47
  isLikedByUser: true,
48
  isDislikedByUser: false,
49
- numberOfLikes: rating.numberOfLikes + 1,
50
- numberOfDislikes: rating.numberOfDislikes - 1,
51
  })
52
  startTransition(async () => {
53
- const freshRating = await rateVideo(video.id, true, huggingfaceApiKey)
54
- setRating(freshRating)
 
 
 
 
55
  })
56
  } : undefined
57
 
58
  const handleDislike = huggingfaceApiKey ? () => {
 
 
59
  setRating({
60
  ...rating,
61
  isLikedByUser: false,
62
  isDislikedByUser: true,
63
- numberOfLikes: rating.numberOfLikes - 1,
64
- numberOfDislikes: rating.numberOfDislikes + 1,
65
  })
66
  startTransition(async () => {
67
- const freshRating = await rateVideo(video.id, false, huggingfaceApiKey)
68
- setRating(freshRating)
 
 
 
 
69
  })
70
  } : undefined
71
 
 
41
  if (!huggingfaceApiKey) { return null }
42
 
43
  const handleLike = huggingfaceApiKey ? () => {
44
+ // we use optimistic updates
45
+ const previousRating = { ...rating }
46
  setRating({
47
  ...rating,
48
  isLikedByUser: true,
49
  isDislikedByUser: false,
50
+ numberOfLikes: Math.abs(Math.max(0, rating.numberOfLikes + 1)),
51
+ numberOfDislikes: Math.abs(Math.max(0, rating.numberOfDislikes - 1)),
52
  })
53
  startTransition(async () => {
54
+ try {
55
+ const freshRating = await rateVideo(video.id, true, huggingfaceApiKey)
56
+ // setRating(freshRating)
57
+ } catch (err) {
58
+ setRating(previousRating)
59
+ }
60
  })
61
  } : undefined
62
 
63
  const handleDislike = huggingfaceApiKey ? () => {
64
+ // we use optimistic updates
65
+ const previousRating = { ...rating }
66
  setRating({
67
  ...rating,
68
  isLikedByUser: false,
69
  isDislikedByUser: true,
70
+ numberOfLikes: Math.abs(Math.max(0, rating.numberOfLikes - 1)),
71
+ numberOfDislikes: Math.abs(Math.max(0, rating.numberOfDislikes + 1)),
72
  })
73
  startTransition(async () => {
74
+ try {
75
+ const freshRating = await rateVideo(video.id, false, huggingfaceApiKey)
76
+ // setRating(freshRating)
77
+ } catch (err) {
78
+ setRating(previousRating)
79
+ }
80
  })
81
  } : undefined
82
 
src/app/interface/top-header/index.tsx CHANGED
@@ -116,7 +116,7 @@ export function TopHeader() {
116
  </div>
117
  </div>
118
  {
119
- isNormalSize ?
120
  <div className={cn(
121
  `hidden sm:flex flex-row space-x-3`,
122
  `text-[13px] font-semibold`,
 
116
  </div>
117
  </div>
118
  {
119
+ isNormalSize && view !== "public_music_videos" ?
120
  <div className={cn(
121
  `hidden sm:flex flex-row space-x-3`,
122
  `text-[13px] font-semibold`,
src/app/interface/track-card/index.tsx CHANGED
@@ -36,6 +36,7 @@ export function TrackCard({
36
  const [shouldLoadMedia, setShouldLoadMedia] = useState(false)
37
 
38
  const isTable = layout === "table"
 
39
  const isCompact = layout === "vertical"
40
 
41
  const handlePointerEnter = () => {
@@ -77,12 +78,14 @@ export function TrackCard({
77
  <div
78
  className={cn(
79
  `w-full flex`,
80
- isTable ? `flex-row h-14 space-x-2 px-2 py-2 rounded-lg` :
81
  isCompact ? `flex-row h-24 py-1 space-x-2` :
82
  `flex-col space-y-3`,
83
  `bg-line-900`,
84
  `cursor-pointer`,
85
- index % 2 ? "bg-neutral-800/40" : "",
 
 
86
  className,
87
  )}
88
  onPointerEnter={handlePointerEnter}
@@ -94,7 +97,8 @@ export function TrackCard({
94
  className={cn(
95
  `flex items-center justify-center`,
96
  `rounded overflow-hidden`,
97
- isTable ? ` flex-col` :
 
98
  isCompact ? ` flex-col w-42 h-42` :
99
  ` flex-col aspect-square`
100
  )}
@@ -172,6 +176,10 @@ export function TrackCard({
172
  {/* TEXT BLOCK */}
173
  <div className={cn(
174
  `flex flex-row`,
 
 
 
 
175
  isCompact ? `w-40 lg:w-44 xl:w-51` : `space-x-4`,
176
  )}>
177
  {
@@ -192,13 +200,15 @@ export function TrackCard({
192
  roundShape
193
  />}
194
  <div className={cn(
195
- `flex flex-col`,
196
- isTable ? `justify-center` :
197
- isCompact ? `` : `flex-grow`
 
198
  )}>
199
  <h3 className={cn(
200
  `text-zinc-100 mb-0 line-clamp-2`,
201
- isTable ? `font-normal text-2xs md:text-xs lg:text-sm mb-0.5` :
 
202
  isCompact ? `font-medium text-2xs md:text-xs lg:text-sm mb-1.5` :
203
  `font-medium text-base`
204
  )}>{media.label}</h3>
@@ -222,6 +232,14 @@ export function TrackCard({
222
  <div className="font-semibold scale-125">·</div>
223
  <div>{formatTimeAgo(media.updatedAt)}</div>
224
  </div>}
 
 
 
 
 
 
 
 
225
  </div>
226
  </div>
227
  </div>
 
36
  const [shouldLoadMedia, setShouldLoadMedia] = useState(false)
37
 
38
  const isTable = layout === "table"
39
+ const isMicro = layout === "micro"
40
  const isCompact = layout === "vertical"
41
 
42
  const handlePointerEnter = () => {
 
78
  <div
79
  className={cn(
80
  `w-full flex`,
81
+ isTable ? `flex-row h-16 space-x-4 px-2 py-2 rounded-lg` :
82
  isCompact ? `flex-row h-24 py-1 space-x-2` :
83
  `flex-col space-y-3`,
84
  `bg-line-900`,
85
  `cursor-pointer`,
86
+ (isTable || isMicro) ? (
87
+ (index % 2) ? "bg-neutral-800/40 hover:bg-neutral-800/70" : "hover:bg-neutral-800/70"
88
+ ) : "",
89
  className,
90
  )}
91
  onPointerEnter={handlePointerEnter}
 
97
  className={cn(
98
  `flex items-center justify-center`,
99
  `rounded overflow-hidden`,
100
+ isTable ? `flex-col` :
101
+ isMicro ? `flex-col` :
102
  isCompact ? ` flex-col w-42 h-42` :
103
  ` flex-col aspect-square`
104
  )}
 
176
  {/* TEXT BLOCK */}
177
  <div className={cn(
178
  `flex flex-row`,
179
+
180
+
181
+ isTable ? `w-full` :
182
+
183
  isCompact ? `w-40 lg:w-44 xl:w-51` : `space-x-4`,
184
  )}>
185
  {
 
200
  roundShape
201
  />}
202
  <div className={cn(
203
+ `flex`,
204
+ isMicro ? ` flex-col justify-center` :
205
+ isTable ? `w-full flex-col md:flex-row justify-center md:justify-start items-start md:items-center` :
206
+ isCompact ? `flex-col` : `flex-col flex-grow`
207
  )}>
208
  <h3 className={cn(
209
  `text-zinc-100 mb-0 line-clamp-2`,
210
+ isMicro ? `font-normal text-2xs md:text-xs lg:text-sm mb-0.5` :
211
+ isTable ? `w-[30%] font-normal text-xs md:text-sm lg:text-base mb-0.5` :
212
  isCompact ? `font-medium text-2xs md:text-xs lg:text-sm mb-1.5` :
213
  `font-medium text-base`
214
  )}>{media.label}</h3>
 
232
  <div className="font-semibold scale-125">·</div>
233
  <div>{formatTimeAgo(media.updatedAt)}</div>
234
  </div>}
235
+
236
+ {/*
237
+ {isTable ? <div className={cn(
238
+ `hidden md:flex flex-row flex-grow`,
239
+ `text-zinc-100 mb-0 line-clamp-2`,
240
+ `w-[30%] font-normal text-xs md:text-sm lg:text-base mb-0.5`
241
+ )}>{media.duration}</div> : null}
242
+ */}
243
  </div>
244
  </div>
245
  </div>
src/app/server/actions/ai-tube-hf/extendVideosWithStats.ts CHANGED
@@ -1,14 +1,24 @@
1
  "use server"
2
 
3
  import { VideoInfo } from "@/types"
4
- import { getNumberOfViewsForVideos } from "../stats"
 
5
 
6
  export async function extendVideosWithStats(videos: VideoInfo[]): Promise<VideoInfo[]> {
7
 
8
- const stats = await getNumberOfViewsForVideos(videos.map(v => v.id))
9
 
10
  return videos.map(v => {
11
- v.numberOfViews = stats[v.id] || 0
 
 
 
 
 
 
 
 
 
12
  return v
13
  })
14
  }
 
1
  "use server"
2
 
3
  import { VideoInfo } from "@/types"
4
+
5
+ import { getStatsForVideos } from "../stats"
6
 
7
  export async function extendVideosWithStats(videos: VideoInfo[]): Promise<VideoInfo[]> {
8
 
9
+ const allStats = await getStatsForVideos(videos.map(v => v.id))
10
 
11
  return videos.map(v => {
12
+ const stats = allStats[v.id] || {
13
+ numberOfViews: 0,
14
+ numberOfLikes: 0,
15
+ numberOfDislikes: 0
16
+ }
17
+
18
+ v.numberOfViews = stats.numberOfViews
19
+ v.numberOfLikes = stats.numberOfLikes
20
+ v.numberOfDislikes = stats.numberOfDislikes
21
+
22
  return v
23
  })
24
  }
src/app/server/actions/ai-tube-hf/getVideo.ts CHANGED
@@ -3,7 +3,7 @@
3
  import { VideoInfo } from "@/types"
4
 
5
  import { getVideoIndex } from "./getVideoIndex"
6
- import { getNumberOfViewsForVideo } from "../stats"
7
 
8
  export async function getVideo({
9
  videoId,
@@ -26,7 +26,17 @@ export async function getVideo({
26
  throw new Error(`cannot get the video, nothing found for id "${id}"`)
27
  }
28
 
29
- video.numberOfViews = await getNumberOfViewsForVideo(video.id)
 
 
 
 
 
 
 
 
 
 
30
 
31
  return video
32
  } catch (err) {
 
3
  import { VideoInfo } from "@/types"
4
 
5
  import { getVideoIndex } from "./getVideoIndex"
6
+ import { getStatsForVideos } from "../stats"
7
 
8
  export async function getVideo({
9
  videoId,
 
26
  throw new Error(`cannot get the video, nothing found for id "${id}"`)
27
  }
28
 
29
+ const allStats = await getStatsForVideos([video.id])
30
+
31
+ const stats = allStats[video.id] || {
32
+ numberOfViews: 0,
33
+ numberOfLikes: 0,
34
+ numberOfDislikes: 0,
35
+ }
36
+
37
+ video.numberOfViews = stats.numberOfViews
38
+ video.numberOfLikes = stats.numberOfLikes
39
+ video.numberOfDislikes = stats.numberOfDislikes
40
 
41
  return video
42
  } catch (err) {
src/app/server/actions/ai-tube-hf/getVideos.ts CHANGED
@@ -4,6 +4,8 @@ import { VideoInfo } from "@/types"
4
 
5
  import { getVideoIndex } from "./getVideoIndex"
6
  import { extendVideosWithStats } from "./extendVideosWithStats"
 
 
7
 
8
  const HARD_LIMIT = 100
9
 
@@ -41,13 +43,16 @@ export async function getVideos({
41
  renewCache: true
42
  })
43
 
44
-
45
  let allPotentiallyValidVideos = Object.values(published)
46
 
47
  if (ignoreVideoIds.length) {
48
  allPotentiallyValidVideos = allPotentiallyValidVideos.filter(video => !ignoreVideoIds.includes(video.id))
49
  }
50
 
 
 
 
 
51
  if (sortBy === "date") {
52
  allPotentiallyValidVideos.sort(((a, b) => b.updatedAt.localeCompare(a.updatedAt)))
53
  } else {
@@ -92,16 +97,22 @@ export async function getVideos({
92
  ]
93
  }
94
  }
95
-
96
 
 
 
97
  // we enforce the max limit of HARD_LIMIT (eg. 100)
98
- const cappedVideos = videosMatchingFilters.slice(0, Math.min(HARD_LIMIT, maxVideos))
99
-
100
-
101
- // finally, we ask Redis for the freshest stats
102
- const videosWithStats = await extendVideosWithStats(cappedVideos)
103
-
104
- return videosWithStats
 
 
 
 
 
105
  } catch (err) {
106
  if (neverThrow) {
107
  console.error("failed to get videos:", err)
 
4
 
5
  import { getVideoIndex } from "./getVideoIndex"
6
  import { extendVideosWithStats } from "./extendVideosWithStats"
7
+ import { isHighQuality } from "../utils/isHighQuality"
8
+ import { isAntisocial } from "../utils/isAntisocial"
9
 
10
  const HARD_LIMIT = 100
11
 
 
43
  renewCache: true
44
  })
45
 
 
46
  let allPotentiallyValidVideos = Object.values(published)
47
 
48
  if (ignoreVideoIds.length) {
49
  allPotentiallyValidVideos = allPotentiallyValidVideos.filter(video => !ignoreVideoIds.includes(video.id))
50
  }
51
 
52
+ if (ignoreVideoIds.length) {
53
+ allPotentiallyValidVideos = allPotentiallyValidVideos.filter(video => !ignoreVideoIds.includes(video.id))
54
+ }
55
+
56
  if (sortBy === "date") {
57
  allPotentiallyValidVideos.sort(((a, b) => b.updatedAt.localeCompare(a.updatedAt)))
58
  } else {
 
97
  ]
98
  }
99
  }
 
100
 
101
+ const sanitizedVideos = videosMatchingFilters.filter(v => !isAntisocial(v))
102
+
103
  // we enforce the max limit of HARD_LIMIT (eg. 100)
104
+ const limitedNumberOfVideos = sanitizedVideos.slice(0, Math.min(HARD_LIMIT, maxVideos))
105
+
106
+ // we ask Redis for the freshest stats
107
+ const videosWithStats = await extendVideosWithStats(limitedNumberOfVideos)
108
+
109
+ const highQuality = videosWithStats.filter(v => isHighQuality(v))
110
+ const lowQuality = videosWithStats.filter(v => !isHighQuality(v))
111
+
112
+ return [
113
+ ...highQuality,
114
+ ...lowQuality
115
+ ]
116
  } catch (err) {
117
  if (neverThrow) {
118
  console.error("failed to get videos:", err)
src/app/server/actions/stats.ts CHANGED
@@ -13,29 +13,38 @@ const redis = new Redis({
13
  token: redisToken
14
  })
15
 
16
- export async function getNumberOfViewsForVideos(videoIds: string[]): Promise<Record<string, number>> {
17
  if (!Array.isArray(videoIds)) {
18
  return {}
19
  }
20
 
21
  try {
22
 
23
- const stats: Record<string, number> = {}
24
 
25
- const ids: string[] = []
26
 
27
  for (const videoId of videoIds) {
28
- ids.push(`videos:${videoId}:stats:views`)
29
- stats[videoId] = 0
 
 
 
 
 
 
30
  }
31
 
32
- const values = await redis.mget<number[]>(...ids)
33
 
34
- values.forEach((nbViews, i) => {
35
- const redisId = `${ids[i] || ""}`
36
- const videoId = redisId.replace(":stats:views", "").replace("videos:", "")
37
- stats[videoId] = nbViews || 0
38
- })
 
 
 
39
 
40
  return stats
41
  } catch (err) {
@@ -43,22 +52,11 @@ export async function getNumberOfViewsForVideos(videoIds: string[]): Promise<Rec
43
  }
44
  }
45
 
46
- export async function getNumberOfViewsForVideo(videoId: string): Promise<number> {
47
- try {
48
- const key = `videos:${videoId}:stats`
49
-
50
- const result = await redis.get<number>(key) || 0
51
-
52
- return result
53
- } catch (err) {
54
- return 0
55
- }
56
- }
57
-
58
-
59
  export async function watchVideo(videoId: string): Promise<number> {
60
  if (developerMode) {
61
- return getNumberOfViewsForVideo(videoId)
 
 
62
  }
63
 
64
  try {
 
13
  token: redisToken
14
  })
15
 
16
+ export async function getStatsForVideos(videoIds: string[]): Promise<Record<string, { numberOfViews: number; numberOfLikes: number; numberOfDislikes: number}>> {
17
  if (!Array.isArray(videoIds)) {
18
  return {}
19
  }
20
 
21
  try {
22
 
23
+ const stats: Record<string, { numberOfViews: number; numberOfLikes: number; numberOfDislikes: number; }> = {}
24
 
25
+ const listOfRedisIDs: string[] = []
26
 
27
  for (const videoId of videoIds) {
28
+ listOfRedisIDs.push(`videos:${videoId}:stats:views`)
29
+ listOfRedisIDs.push(`videos:${videoId}:stats:likes`)
30
+ listOfRedisIDs.push(`videos:${videoId}:stats:dislikes`)
31
+ stats[videoId] = {
32
+ numberOfViews: 0,
33
+ numberOfLikes: 0,
34
+ numberOfDislikes: 0,
35
+ }
36
  }
37
 
38
+ const listOfRedisValues = await redis.mget<number[]>(...listOfRedisIDs)
39
 
40
+ let v = 0
41
+ for (let i = 0; i < listOfRedisValues.length; i += 3) {
42
+ stats[videoIds[v++]] = {
43
+ numberOfViews: listOfRedisValues[i] || 0,
44
+ numberOfLikes: listOfRedisValues[i + 1] || 0,
45
+ numberOfDislikes: listOfRedisValues[i + 2] || 0
46
+ }
47
+ }
48
 
49
  return stats
50
  } catch (err) {
 
52
  }
53
  }
54
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  export async function watchVideo(videoId: string): Promise<number> {
56
  if (developerMode) {
57
+ const stats = await getStatsForVideos([videoId])
58
+
59
+ return stats[videoId].numberOfViews
60
  }
61
 
62
  try {
src/app/server/actions/utils/isAntisocial.ts ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { VideoInfo } from "@/types"
2
+
3
+ const winners = new Set(`${process.env.WINNERS || ""}`.toLowerCase().split(",").map(x => x.trim()).filter(x => x))
4
+
5
+ export function isAntisocial(video: VideoInfo): boolean {
6
+
7
+ // some people are reported by the community for their anti-social behavior
8
+ // this include:
9
+ // - harassing
10
+ //
11
+ // - annoying or not letting people in peace on social networks
12
+ // (keep trying to reach with multiple user accounts etc)
13
+ //
14
+ // - stealing other people content (prompt, identity, images etc)
15
+ //
16
+ // -- creating multiple/duplicate accounts in order to foil and get around AiTube bans
17
+ //
18
+ // - generating nonsense content (eg. sentences not finished, one letter titles)
19
+ //
20
+ // - duplicate many videos with little to no changes
21
+ // (TV series are of course an exception to this rule - as long as this is original content obviously)
22
+ return winners.has(video.channel.datasetUser.toLowerCase())
23
+ }
src/app/server/actions/utils/isHighQuality.ts ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { VideoInfo } from "@/types"
2
+
3
+ export function isHighQuality(video: VideoInfo) {
4
+ const numberOfViews = Math.abs(Math.max(0, video.numberOfViews))
5
+ const numberOfLikes = Math.abs(Math.max(0, video.numberOfLikes))
6
+ const numberOfDislikes = Math.abs(Math.max(0, video.numberOfDislikes))
7
+
8
+
9
+ // rock star videos will quickly reach high ratings
10
+ const isVeryPopular = numberOfViews > 100000 || numberOfLikes > 100000
11
+
12
+ if (isVeryPopular) { return true }
13
+
14
+ const rating = numberOfLikes - numberOfDislikes
15
+
16
+ // while the number of dislike should be enough, some content is so bad that
17
+ // people don't even take the time to watch and dislike it
18
+ // so we might add other roules
19
+ const isAppreciatedByPeople = rating > 0
20
+
21
+ return isAppreciatedByPeople
22
+ }
src/types.ts CHANGED
@@ -650,6 +650,7 @@ export type MediaDisplayLayout =
650
  | "horizontal" // will be used for a "Netflix" horizontal sliding mode
651
  | "vertical" // used in the right recommendation panel
652
  | "table" // used when shown in a table mode
 
653
 
654
  export type VideoRating = {
655
  isLikedByUser: boolean
 
650
  | "horizontal" // will be used for a "Netflix" horizontal sliding mode
651
  | "vertical" // used in the right recommendation panel
652
  | "table" // used when shown in a table mode
653
+ | "micro"
654
 
655
  export type VideoRating = {
656
  isLikedByUser: boolean