Skip to content

Statistics: fix getCurrentBookReadPages() when use_pagemap_for_stats#14426

Closed
poire-z wants to merge 1 commit into
koreader:masterfrom
poire-z:fix_pagemap_stats
Closed

Statistics: fix getCurrentBookReadPages() when use_pagemap_for_stats#14426
poire-z wants to merge 1 commit into
koreader:masterfrom
poire-z:fix_pagemap_stats

Conversation

@poire-z

@poire-z poire-z commented Oct 6, 2025

Copy link
Copy Markdown
Contributor

Fix #14388 (comment).

@jospalau : can you check this version too ? (same SQL, just proper if/else branches, but just want to double check it is fine).


This change is Reviewable

@jospalau

jospalau commented Oct 7, 2025

Copy link
Copy Markdown

I have been testing while reading. The read pages are a little bit behind:
2025-10-07-0726_grim

I tried not scaling the reference pages and transforming them later using the pagemap table:

    if self.use_pagemap_for_stats then
        local sql_stmt = [[
            SELECT
                page,
                min(sum(duration), ?) AS durations,
                strftime("%s", "now") - max(start_time) AS delay
            FROM (
                SELECT floor(first_page + idx - 1) AS page, start_time, duration / (last_page - first_page + 1) AS duration
                FROM (
                    SELECT start_time, duration,
                        -- First page_number for this page after rescaling single row
                        (page - 1) + 1 AS first_page,
                        -- Last page_number for this page after rescaling single row
                        max((page - 1) + 1, page) AS last_page,
                        idx
                    FROM page_stat_data
                    -- Duplicate rows for multiple pages as needed (as a result of rescaling)
                    JOIN (SELECT number as idx FROM numbers) AS N ON idx <= (last_page - first_page + 1)
                    WHERE id_book = ?
                )
            )
            GROUP BY page
            ORDER BY page;
        ]]

        conn = SQ3.open(db_location)
        stmt = conn:prepare(sql_stmt)
        res, nb = stmt:reset():bind(self.settings.max_sec, self.id_curr_book):resultset("i")
        -- logger.warn(res)
        stmt:close()
        conn:close()
    end

And then later:

    for i=1, nb do
        local page, duration, delay = res[1][i], res[2][i], res[3][i]
        page = tonumber(page)
        if self.use_pagemap_for_stats and self.ui.document:getPageMap()[page] then
            -- print(page .. " - " .. self.ui.document:getPageMap()[page].page)
            page = self.ui.document:getPageMap()[page].page
        end

There are some gaps for those synthetic pages not having a reference page (more screen pages than synthetic ones) but it seems fine:
2025-10-07-0729_grim

@poire-z

poire-z commented Oct 7, 2025

Copy link
Copy Markdown
Contributor Author

Any idea why the differences ? Why these 4 missing slots on the first screenshot ?

I tried not scaling the reference pages and transforming them later using the pagemap table:

It feels this might be more expensive.

Not sure it was really a good idea to store ref pages - the possible holes in Bookmap makes it really annoying and feel like a regression.
Dunno if we could just have kept storing screen pages, and just compute some ref pages stats from that in the (possibly many) other places...

@jospalau

jospalau commented Oct 7, 2025

Copy link
Copy Markdown

Any idea why the differences ? Why these 4 missing slots on the first screenshot ?

To be honest I don' t know why. I see you added the db modified view in Lua code to use screen pages and for some reason the result of the scaled page gets behind. Maybe it has to do with there is no 1-1 mapping when using reference pages.

I particularly don't care if the reference pages are stored in the book table since I use precalculated word count number from Calibre to measure how big a book is and % is enough to track. Although, I still find reference pages valuable to see consistently which page I am, how many pages left while reading...

@jospalau

jospalau commented Oct 7, 2025

Copy link
Copy Markdown

I read 3 pages of a book after skipping the first pages (table of contents, acknowledments...) and this is the result:
2025-10-07-1834_grim

This happens for nearly all books, because there are many pages at the beginning with not many characters.

I modified the book file removing the first pages:
$ ebook-edit save/King,\ Stephen/It\ -\ King,\ Stephen.epub

Then removed db info for the book and the cache folder and repeat reading the same pages:
2025-10-07-1837_grim

It is consistent but as you keep reading it will vary because it is not linear. The approach I commented using the pagemap seems to be working fine. I actually started using the Book map to see which page I am in a chapter visually.

@mergen3107

Copy link
Copy Markdown
Contributor

Sorry guys, a random idea, but I think (I hope) it might help:
What if such pages that were merely skimmed through (which where "read" less than the page count threshold) were represented not a columns, but as dots?

And those won't go into actual statistics, because you didn't actually read them.

@Frenzie

Frenzie commented Oct 7, 2025

Copy link
Copy Markdown
Member

Isn't that already what happens? If you consider the default of 5 s (?) too low you can always raise it.

@mergen3107

mergen3107 commented Oct 7, 2025

Copy link
Copy Markdown
Contributor

Yes, but I emphasized the visual aspect of it.

  • If a page was never opened, it stays blank (0 seconds or being read).
  • If it was skimmed, it has a dot and doesn't go into statistics (between 0 s and theeshold).
  • If it was read for real (above threshold) then it has the regular bars.

@poire-z

poire-z commented Oct 7, 2025

Copy link
Copy Markdown
Contributor Author

This happens for nearly all books, because there are many pages at the beginning with not many characters.

I see. This is a problem with our synthetic page map builder that does not know about page breaks :/

Does this happen also with real reference pages ?

@jospalau

jospalau commented Oct 7, 2025

Copy link
Copy Markdown

Yes, I initially tried with one having reference pages.

@poire-z

poire-z commented Oct 7, 2025

Copy link
Copy Markdown
Contributor Author

And then later:

    for i=1, nb do
        local page, duration, delay = res[1][i], res[2][i], res[3][i]
        page = tonumber(page)
        if self.use_pagemap_for_stats and self.ui.document:getPageMap()[page] then
            -- print(page .. " - " .. self.ui.document:getPageMap()[page].page)
            page = self.ui.document:getPageMap()[page].page
        end

You didn't show what's after, but if you didn't change it:
read_pages[page] = {duration, delay}
As the page you get from self.ui.document:getPageMap()[page].page may be the same for the next one (?), you would override {duration, delay}` from the previous page.
The SQL view seems to take care of "merging/flattening" it all and giving us unique pages - that this trick may kill...

@jospalau

jospalau commented Oct 7, 2025

Copy link
Copy Markdown

I just added that conditional and yes, this trick it is not accurate. There are gaps from time to time.

@poire-z

poire-z commented Oct 8, 2025

Copy link
Copy Markdown
Contributor Author

@hius07 : any thought about this mess (here, #14405 (comment)) about using ref/synthetic pages in statistics ?

My gut feeling is that we were just fine/better with rendered pages - there can be so many problem with ref and synthetic pages vs. BookMap & PageBrowser.
Dunno if it wouldn't be better to do any conversion from rendered pages in statistics to ref/synth page numbers in the few places where we would want to display them in the statistics KVPages - or if I'm missing some other situations.

@hius07

hius07 commented Oct 9, 2025

Copy link
Copy Markdown
Member

(Sorry, I don't use and don't know the book map well)

I think we cannot calculate the reading time of a screen page based on the statistics of reference pages,
Also we cannot show in the book map which screen pages (as a partial ref page) has been read.
Does it make sense to build a book map with horizontal axis based on the reference pages?

@hius07

hius07 commented Oct 9, 2025

Copy link
Copy Markdown
Member

But I imagine using reference pages in the statistics just as a changing of book formatting (font size etc.).
It is supported and should work well there.

@poire-z

poire-z commented Oct 9, 2025

Copy link
Copy Markdown
Contributor Author

Does it make sense to build a book map with horizontal axis based on the reference pages?

BookMap interacts with (and use bits from) PageBrowser and works on screen pages.
I don't even want to think about them using reference pages as page slots (for the little I care about ref pages).

But I imagine using reference pages in the statistics just as a changing of book formatting (font size etc.).
It is supported and should work well there.

It works - as shown here and around, with caveats: holes, ref pages possibly not spanning the whole book (try @jonnyl2 's provided test epub, I can't even make sense of what I see and the statistics...
image
)
With rendered pages, and changing formatting, it may look odd (holes) if you make really strong changes (small to large size or vice versa) - but it's usually something you do at a start of a book, so there's no "old baggage/bad stats" to carry/scale - and the tracking of your reading may have a few rare holes or shifts (ie. small reading time on the last page of a chapter being pushed to another near page slot), but it's usually fine.
With synth pages, you may get #14426 (comment).

I think we cannot calculate the reading time of a screen page based on the statistics of reference pages,
Also we cannot show in the book map which screen pages (as a partial ref page) has been read.

That's what the "scaling" (of stats page to screen pages) is supposed to do. It will work somehow (with the caveats mentionned), but it's not super nice and doesn't track your reading (progress, with holes - and position, with possible shifts) well.

@hius07

hius07 commented Oct 9, 2025

Copy link
Copy Markdown
Member

I mean, let's assume the statistics base has some records for ref page [97].
Corresponding screen pages of the whole ref page [97] are, say, ten pages 120 - 129.
We get total reading time for ref page [97] and BookMap shows all pages 120 - 129 with the equal bars, no gaps.

The above screenshot shows ref page [97] partially filled, but from the ref pages statistics we don't know which screen page actually has been read.

@poire-z

poire-z commented Oct 9, 2025

Copy link
Copy Markdown
Contributor Author

So ? (You described one of the issues.)

@hius07

hius07 commented Oct 9, 2025

Copy link
Copy Markdown
Member

Fill all of screen pages that are included into one ref page with the same bar.

@hius07

hius07 commented Oct 9, 2025

Copy link
Copy Markdown
Member

Again, from the ref pages statistics we don't know which screen page has been read, and scaling cannot help.

@poire-z

poire-z commented Oct 9, 2025

Copy link
Copy Markdown
Contributor Author

That's what the scaling is already doing (this PR) - and what is shown in the screenshot just above.

In the example above, it's even worse: page 97 spans... like 30 screen pages.
You will have it as being read 2 minutes if only 3 pages are read. As 20 minutes if the 30 pages are read. It will show small or tall slots on the 30 pages, while you may not have actually read them.

There are so many cases where things can go worse in different ways:

  • ref/synth pages spanning 3 screen pages: reading time for a page accumulate, this might be ok, but you'll have a lot of 3-pages same-height segments, and your current position lag by 1 or 2 slots
  • 3 ref/synth pages on a screen page: 2 pages will have no reading time
  • (well, I'm tired of trying to imagine new cases about this mess)

Again, from the ref pages statistics we don't know which screen page has been read, and scaling cannot help.

Exactly ! And my wish we revert this, and go at just adapting in the statistics display the total ref/synth page count, reading ref/page percent, reading time per ref/synth page, etc...

@hius07

hius07 commented Oct 9, 2025

Copy link
Copy Markdown
Member

It will show small or tall slots on the 30 pages, while you may not have actually read them.

That's the question to the book publisher.
And that's what the users wanted when requesting ref pages statistics - they were reading page [97] all the time and saw this fact everywhere.

I hope there will be no issue with accurate synthetic pages.

In any case, I do not fight for this feature, ready to revert it.

@poire-z

poire-z commented Oct 9, 2025

Copy link
Copy Markdown
Contributor Author

That's the question to the book publisher.

Except it gave us a book and is gone and can't be reached :) and we have to do with it, and give our users something usable if we can.

And that's what the users wanted when requesting ref pages statistics - they were reading page [97] all the time and saw this fact everywhere.

That's what they'll see in the footer & co.
Currently in our ref page statistics, we have no knowledge that page [96] spans 2 screen pages, and that page [97] spans 30 screen pages. The scaling will try to scale linearly, and screen page slots won't/can't reflect the reality.

I hope there will be no issue with accurate synthetic pages.

Possibly less (because they should span the whole book - unlike ref pages that can stop), but you may still have some if some screen pages are filled with pictures and no text, and you may have a synth page spanning 20 screen pages, while on other pages with text, you'll have 2 synth pages per screen pages...

If we go at storing screen pages in statistics, we then only have to deal with numbers shown in KVPage - we can do what we can, but if we fail, they just won't be that accurate, they should look coherent anyway - unlike what we see in BookMap and PageBrowser where it does hurt your eyes.

@poire-z

poire-z commented Oct 10, 2025

Copy link
Copy Markdown
Contributor Author

No longer necessary after we reverted the cause in #14435.

@poire-z poire-z closed this Oct 10, 2025
@poire-z poire-z deleted the fix_pagemap_stats branch October 10, 2025 20:04
@poire-z

poire-z commented Oct 10, 2025

Copy link
Copy Markdown
Contributor Author

Mentionning that here, because it's really noticable in the screenshot above at #14426 (comment) :

In crengine's ldomDocument::buildSyntheticPageMap(), I noted this:

    // We loop thru all text nodes, whether visible or not (as visiblity can be changed
    // with styles, and we want this to be stable)
    while ( xp.nextText() ) {
        // But skip text from elements which would by default be hidden (<style>, <script>,
        // some FB2 elements...), mostly so that a huge <style> at top of document doesn't
        // generate many synthetic pages that would end up all on the first screen page.
        const css_elem_def_props_t * ntype = xp.getNode()->getParentNode()->getElementTypePtr();
        if ( ntype && ntype->display == css_d_none ) {
            continue;
        }
        lString32 txt = xp.getText();
[...]

I still don't want to have to look at the publisher styles for any explicite page-break, that we could account for by emitting a new page number - because users can disable or tweak publisher styles, and I really want synthetic page numbers to be stable whatever the styles change.
But there's one thing stable we may account for: DocFragments - as each new HTML file could start a new page number (it's 90% possible that in that screenshots, each of the page at the beginning is its own DocFragment. (In some books I've seen, it may happen that DocFragments can restart anywhere between 2 paragraphs, but this may be super rare.
I'm still not sure how to do that, but I guess I have a few days to see before @hius07 #14405 is merged.

@poire-z

poire-z commented Oct 11, 2025

Copy link
Copy Markdown
Contributor Author

Before emitting a new synthetic page number when we meet a new DocFragment:
image

After:
image

After, zoomed on some pages to show them in margins:
image
(The page number in margin is shown when the text on that page starts - this is a bit odd, I would expect it at the top, but that's already the case with Reference page numbers, and another issue if it is one.)

@dmalinovsky @jospalau @hius07 (and others who like synthetic page numbers):
Which do you prefer ?
"Before" was giving an accurate total page count according to the amount of text to read.
"After" will give a bit more as it will create new page numbers at the beginning and in between top chapters for each "title/acknoledgement/chapter" page, but it makes it a bit more real-book like by wasting a few page numbers at the beginning for all the title/editor nearly blank pages (and possibly in between chapters/parts), so the actual content starts on page 7 instead of 3 - and it looks better in BookMap.
Both will keep being stable between devices, like before. (Except for your past books you had with the patch, you would now get a few more pages.)

@dmalinovsky

Copy link
Copy Markdown
Contributor

Doesn’t matter honestly, as long as this number is consistent. Whatever makes more sense for you.

@jospalau

Copy link
Copy Markdown

Same.

I need reference pages to have consistent "page" numbers in all my books but for statistics I don't mind. Actually, I put attention to screen pages for chapter pages when reading.

@jonnyl2

jonnyl2 commented Oct 17, 2025

Copy link
Copy Markdown
Contributor

@poire-z : Any advice why this counts as 3 new pages? (This is from the book shared here). There are no new html files in between.

(Set to synthetic pages @ 500 cpp.)

image

@poire-z

poire-z commented Oct 17, 2025

Copy link
Copy Markdown
Contributor Author

No idea yet, may be I screwed up the counting while changing the way we do it (needed to account for DocFragments)...
I can reproduce (this + some smaller sections with less than 500 chars).

@poire-z

poire-z commented Oct 17, 2025

Copy link
Copy Markdown
Contributor Author

No such issue with the previous code :/

@poire-z

poire-z commented Oct 17, 2025

Copy link
Copy Markdown
Contributor Author

OK, obviously I didn't test if with paragraphs larger than the chars_per_page :/

Still not heavily tested, but this reads better:

--- a/crengine/src/lvtinydom.cpp
+++ b/crengine/src/lvtinydom.cpp
@@ -20999,14 +20999,17 @@ void ldomDocument::buildSyntheticPageMap( int chars_per_synthetic_page )
                 else { // isText()
                     lString32 text = n->getText().trim();
                     int len = text.length();
+                    int offset = 0;
+printf("    %d %d %s\n", len, count, UnicodeToUtf8(ldomXPointer(n, offset).toString()).c_str());
                     while (len > count) {
-                        int offset = count;
+                        offset += count;
                         len -= count;
                         count = chars_per_synthetic_page;
                         page_num++;
                         lString32 title;
                         title.appendDecimal(page_num);
                         m_pagemap.addPage(title, ldomXPointer(n, offset), lString32::empty_str);
+printf("%03d %d %d %s\n", page_num, len, count, UnicodeToUtf8(ldomXPointer(n, offset).toString()).c_str());
                     }
                     count -= len;
                 }

Before (with only these printf()):

    23 101 /body/DocFragment[2]/body/p[24]/em/text().23
    845 78 /body/DocFragment[2]/body/p[24]/text().845
035 767 500 /body/DocFragment[2]/body/p[24]/text().78
036 267 500 /body/DocFragment[2]/body/p[24]/text().500
    7 233 /body/DocFragment[2]/body/p[25]/text()[1].7
    1318 226 /body/DocFragment[2]/body/p[25]/text()[2].1318
037 1092 500 /body/DocFragment[2]/body/p[25]/text()[2].226
038 592 500 /body/DocFragment[2]/body/p[25]/text()[2].500
039 92 500 /body/DocFragment[2]/body/p[25]/text()[2].500
    33 408 /body/DocFragment[2]/body/div[5]/span[1]/text()[1].33
    0 375 /body/DocFragment[2]/body/div[5]/span[1]/text()[2].0
    36 375 /body/DocFragment[2]/body/div[5]/span[2]/text()[1].36
    0 339 /body/DocFragment[2]/body/div[5]/span[2]/text()[2].0
    27 339 /body/DocFragment[2]/body/p[26]/text().27
    41 312 /body/DocFragment[2]/body/div[6]/div/span[1]/text()[1].41
    0 271 /body/DocFragment[2]/body/div[6]/div/span[1]/text()[2].0
    33 271 /body/DocFragment[2]/body/div[6]/div/span[2]/text()[1].33
    0 238 /body/DocFragment[2]/body/div[6]/div/span[2]/text()[2].0
    22 238 /body/DocFragment[2]/body/div[6]/div/span[3]/text()[1].22
    0 216 /body/DocFragment[2]/body/div[6]/div/span[3]/text()[2].0
    36 216 /body/DocFragment[2]/body/div[6]/div/span[4]/text()[1].36
    0 180 /body/DocFragment[2]/body/div[6]/div/span[4]/text()[2].0
    59 180 /body/DocFragment[2]/body/p[27]/text().59
    15 121 /body/DocFragment[2]/body/div[7]/div/span[1]/text()[1].15
    0 106 /body/DocFragment[2]/body/div[7]/div/span[1]/text()[2].0
    31 106 /body/DocFragment[2]/body/div[7]/div/span[2]/text()[1].31
    0 75 /body/DocFragment[2]/body/div[7]/div/span[2]/text()[2].0
    1986 75 /body/DocFragment[2]/body/p[28]/text()[1].1986
040 1911 500 /body/DocFragment[2]/body/p[28]/text()[1].75
041 1411 500 /body/DocFragment[2]/body/p[28]/text()[1].500 these 3 points to the same target/xpointer
042 911 500 /body/DocFragment[2]/body/p[28]/text()[1].500
043 411 500 /body/DocFragment[2]/body/p[28]/text()[1].500
    93 89 /body/DocFragment[2]/body/p[28]/text()[2].93
044 4 500 /body/DocFragment[2]/body/p[28]/text()[2].89
    1535 496 /body/DocFragment[2]/body/p[29]/text()[1].1535
045 1039 500 /body/DocFragment[2]/body/p[29]/text()[1].496
046 539 500 /body/DocFragment[2]/body/p[29]/text()[1].500
047 39 500 /body/DocFragment[2]/body/p[29]/text()[1].500
    21 461 /body/DocFragment[2]/body/p[29]/em[1]/text().21
    97 440 /body/DocFragment[2]/body/p[29]/text()[2].97
    1109 343 /body/DocFragment[2]/body/p[29]/text()[3].1109
048 766 500 /body/DocFragment[2]/body/p[29]/text()[3].343

After:

034 399 500 /body/DocFragment[2]/body/p[23]/text()[3].669
    23 101 /body/DocFragment[2]/body/p[24]/em/text().0
    845 78 /body/DocFragment[2]/body/p[24]/text().0
035 767 500 /body/DocFragment[2]/body/p[24]/text().78
036 267 500 /body/DocFragment[2]/body/p[24]/text().578
    7 233 /body/DocFragment[2]/body/p[25]/text()[1].0
    1318 226 /body/DocFragment[2]/body/p[25]/text()[2].0
037 1092 500 /body/DocFragment[2]/body/p[25]/text()[2].226
038 592 500 /body/DocFragment[2]/body/p[25]/text()[2].726
039 92 500 /body/DocFragment[2]/body/p[25]/text()[2].1226
    33 408 /body/DocFragment[2]/body/div[5]/span[1]/text()[1].0
    0 375 /body/DocFragment[2]/body/div[5]/span[1]/text()[2].0
    36 375 /body/DocFragment[2]/body/div[5]/span[2]/text()[1].0
    0 339 /body/DocFragment[2]/body/div[5]/span[2]/text()[2].0
    27 339 /body/DocFragment[2]/body/p[26]/text().0
    41 312 /body/DocFragment[2]/body/div[6]/div/span[1]/text()[1].0
    0 271 /body/DocFragment[2]/body/div[6]/div/span[1]/text()[2].0
    33 271 /body/DocFragment[2]/body/div[6]/div/span[2]/text()[1].0
    0 238 /body/DocFragment[2]/body/div[6]/div/span[2]/text()[2].0
    22 238 /body/DocFragment[2]/body/div[6]/div/span[3]/text()[1].0
    0 216 /body/DocFragment[2]/body/div[6]/div/span[3]/text()[2].0
    36 216 /body/DocFragment[2]/body/div[6]/div/span[4]/text()[1].0
    0 180 /body/DocFragment[2]/body/div[6]/div/span[4]/text()[2].0
    59 180 /body/DocFragment[2]/body/p[27]/text().0
    15 121 /body/DocFragment[2]/body/div[7]/div/span[1]/text()[1].0
    0 106 /body/DocFragment[2]/body/div[7]/div/span[1]/text()[2].0
    31 106 /body/DocFragment[2]/body/div[7]/div/span[2]/text()[1].0
    0 75 /body/DocFragment[2]/body/div[7]/div/span[2]/text()[2].0
    1986 75 /body/DocFragment[2]/body/p[28]/text()[1].0
040 1911 500 /body/DocFragment[2]/body/p[28]/text()[1].75
041 1411 500 /body/DocFragment[2]/body/p[28]/text()[1].575    looks better !
042 911 500 /body/DocFragment[2]/body/p[28]/text()[1].1075
043 411 500 /body/DocFragment[2]/body/p[28]/text()[1].1575
    93 89 /body/DocFragment[2]/body/p[28]/text()[2].0
044 4 500 /body/DocFragment[2]/body/p[28]/text()[2].89
    1535 496 /body/DocFragment[2]/body/p[29]/text()[1].0
045 1039 500 /body/DocFragment[2]/body/p[29]/text()[1].496
046 539 500 /body/DocFragment[2]/body/p[29]/text()[1].996
047 39 500 /body/DocFragment[2]/body/p[29]/text()[1].1496
    21 461 /body/DocFragment[2]/body/p[29]/em[1]/text().0
    97 440 /body/DocFragment[2]/body/p[29]/text()[2].0
    1109 343 /body/DocFragment[2]/body/p[29]/text()[3].0
048 766 500 /body/DocFragment[2]/body/p[29]/text()[3].343
049 266 500 /body/DocFragment[2]/body/p[29]/text()[3].843

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants