BookBERTMultimodal2 class. It treats a comic book as a "sequence" of pages and uses a Transformer encoder to understand the context of a page based on its position in the book.1152-dim): Extracted using SigLIP (google/siglip-so400m-patch14-384).1024-dim): Extracted from OCR text using Qwen-Embedding (Qwen/Qwen3-Embedding-0.6B).(Dim -> 3840 -> 1920 -> 768) align both visual and text features into a common 768-dim space.Transformers.BertModel) processes the combined features across the entire length of the comic book, allowing the model to understand that an advertisement usually follows a story page, or credits appear at the end.768-dim token back to one of 9 distinct classes.advertisementcoverstory (The primary narrative content)textstoryfirst-pagecreditsart (Splash pages, pin-ups)text (Editorial text)back_coversrc/cosmo/.1152-d) and text (1024-d) embeddings for a sequence of pages, you can run inference like this:1import torch
2import torch.nn as nn
3from transformers import BertConfig, BertModel
4
5# 1. Define Architecture (Must match exactly)
6class BookBERT(nn.Module):
7 def __init__(self, bert_input=768, num_classes=9, hidden_dim=512, dropout_p=0.0):
8 super().__init__()
9 config = BertConfig(
10 hidden_size=bert_input, num_hidden_layers=4, num_attention_heads=4,
11 intermediate_size=bert_input * 4, max_position_embeddings=1024
12 )
13 self.bert_encoder = BertModel(config)
14 self.classifier = nn.Sequential(
15 nn.Linear(bert_input, hidden_dim),
16 nn.Linear(hidden_dim, hidden_dim // 2),
17 nn.LayerNorm(hidden_dim // 2),
18 nn.GELU(),
19 nn.Dropout(dropout_p),
20 nn.Linear(hidden_dim // 2, hidden_dim // 4),
21 nn.LayerNorm(hidden_dim // 4),
22 nn.GELU(),
23 nn.Dropout(dropout_p),
24 nn.Linear(hidden_dim // 4, num_classes)
25 )
26
27class BookBERTMultimodal2(BookBERT):
28 def __init__(self, textual_dim=1024, visual_dim=1152, bert_dim=768, classes=9):
29 super().__init__(bert_input=bert_dim, num_classes=classes, hidden_dim=512, dropout_p=0.0)
30
31 sz1_v = (visual_dim + bert_dim) * 2
32 self.visual_projection = nn.Sequential(
33 nn.Linear(visual_dim, sz1_v), nn.LayerNorm(sz1_v), nn.GELU(), nn.Dropout(0.0),
34 nn.Linear(sz1_v, sz1_v//2), nn.LayerNorm(sz1_v//2), nn.GELU(), nn.Dropout(0.0),
35 nn.Linear(sz1_v//2, bert_dim)
36 )
37
38 sz1_t = (textual_dim + bert_dim) * 2
39 self.textual_projection = nn.Sequential(
40 nn.Linear(textual_dim, sz1_t), nn.LayerNorm(sz1_t), nn.GELU(), nn.Dropout(0.0),
41 nn.Linear(sz1_t, sz1_t//2), nn.LayerNorm(sz1_t//2), nn.GELU(), nn.Dropout(0.0),
42 nn.Linear(sz1_t//2, bert_dim)
43 )
44 self.norm = nn.LayerNorm(bert_dim)
45
46 def forward(self, textual_features, visual_features):
47 batch_size, seq_len, _ = textual_features.shape
48 mask = torch.ones((batch_size, seq_len), device=textual_features.device)
49
50 t_norm = self.norm(self.textual_projection(textual_features))
51 v_norm = self.norm(self.visual_projection(visual_features))
52
53 combined = torch.stack([t_norm, v_norm], dim=2).view(batch_size, seq_len * 2, -1)
54 exp_mask = mask.unsqueeze(2).expand(-1, -1, 2).reshape(batch_size, seq_len * 2)
55
56 bert_out = self.bert_encoder(inputs_embeds=combined, attention_mask=exp_mask)
57 reshaped = bert_out.last_hidden_state.view(batch_size, seq_len, 2, -1)
58 return self.classifier(reshaped[:, :, -1, :])
59
60# 2. Load Model
61device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
62model = BookBERTMultimodal2().to(device)
63
64state_dict = torch.hub.load_state_dict_from_url(
65 "https://huggingface.co/RichardScottOZ/cosmo-v4/resolve/main/best_Multimodal_MultiToken_v4.pt",
66 map_location=device
67)
68if 'model_state_dict' in state_dict:
69 state_dict = state_dict['model_state_dict']
70model.load_state_dict(state_dict, strict=True)
71model.eval()
72
73# 3. Inference (Example: 1 comic book containing 24 pages)
74# visual_embeddings shape: (1, 24, 1152) -> From SigLIP
75# text_embeddings shape: (1, 24, 1024) -> From Qwen
76visual_embs = torch.randn(1, 24, 1152).to(device)
77text_embs = torch.randn(1, 24, 1024).to(device)
78
79with torch.inference_mode():
80 logits = model(text_embs, visual_embs)
81 predictions = torch.argmax(logits, dim=-1).squeeze(0)
82
83class_names = ["advertisement", "cover", "story", "textstory", "first-page", "credits", "art", "text", "back_cover"]
84for page_num, pred_idx in enumerate(predictions):
85 print(f"Page {page_num}: {class_names[pred_idx]}")1024 tokens, equating to 512 pages per forward pass. For massive omnibuses, chunking is required.1
2--- Predictions (First 25) ---
3Page ID | Label
4--------------------------------------------------------------------------------
5#Guardian 001_#Guardian 001 - p000.jpg | cover
6#Guardian 001_#Guardian 001 - p001.jpg | text
7#Guardian 001_#Guardian 001 - p002.jpg | story
8#Guardian 001_#Guardian 001 - p003.jpg | story
9#Guardian 001_#Guardian 001 - p004.jpg | story
10#Guardian 001_#Guardian 001 - p005.jpg | story
11#Guardian 001_#Guardian 001 - p006.jpg | story
12#Guardian 001_#Guardian 001 - p007.jpg | story
13#Guardian 001_#Guardian 001 - p008.jpg | story
14#Guardian 001_#Guardian 001 - p009.jpg | advertisement
15#Guardian 001_#Guardian 001 - p010.jpg | story
16#Guardian 001_#Guardian 001 - p011.jpg | story
17#Guardian 001_#Guardian 001 - p012.jpg | story
18#Guardian 001_#Guardian 001 - p013.jpg | story
19#Guardian 001_#Guardian 001 - p014.jpg | story
20#Guardian 001_#Guardian 001 - p015.jpg | story
21#Guardian 001_#Guardian 001 - p016.jpg | story
22#Guardian 001_#Guardian 001 - p017.jpg | story
23#Guardian 001_#Guardian 001 - p018.jpg | story
24#Guardian 001_#Guardian 001 - p019.jpg | story
25#Guardian 001_#Guardian 001 - p020.jpg | story
26#Guardian 001_#Guardian 001 - p021.jpg | story
27#Guardian 001_#Guardian 001 - p022.jpg | story
28#Guardian 001_#Guardian 001 - p023.jpg | story
29#Guardian 001_#Guardian 001 - p024.jpg | text